mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 14:51:57 +00:00
Support token auth
This commit is contained in:
parent
752ddff01f
commit
16888e54ee
@ -3,6 +3,7 @@ package biz
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/cuigh/auxo/data"
|
||||||
"github.com/cuigh/auxo/errors"
|
"github.com/cuigh/auxo/errors"
|
||||||
"github.com/cuigh/auxo/net/web"
|
"github.com/cuigh/auxo/net/web"
|
||||||
"github.com/cuigh/auxo/security/passwd"
|
"github.com/cuigh/auxo/security/passwd"
|
||||||
@ -30,6 +31,7 @@ type UserBiz interface {
|
|||||||
Update(user *dao.User, ctxUser web.User) (err error)
|
Update(user *dao.User, ctxUser web.User) (err error)
|
||||||
FindByID(id string) (user *dao.User, err error)
|
FindByID(id string) (user *dao.User, err error)
|
||||||
FindByName(loginName string) (user *dao.User, err error)
|
FindByName(loginName string) (user *dao.User, err error)
|
||||||
|
FindByToken(token string) (user *dao.User, err error)
|
||||||
FindPrivacy(loginName string) (privacy *UserPrivacy, err error)
|
FindPrivacy(loginName string) (privacy *UserPrivacy, err error)
|
||||||
Count() (count int, err error)
|
Count() (count int, err error)
|
||||||
Delete(id, name string, user web.User) (err error)
|
Delete(id, name string, user web.User) (err error)
|
||||||
@ -76,6 +78,10 @@ func (b *userBiz) FindByName(loginName string) (user *dao.User, err error) {
|
|||||||
return b.d.UserGetByName(context.TODO(), loginName)
|
return b.d.UserGetByName(context.TODO(), loginName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *userBiz) FindByToken(token string) (user *dao.User, err error) {
|
||||||
|
return b.d.UserGetByToken(context.TODO(), token)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *userBiz) FindPrivacy(loginName string) (privacy *UserPrivacy, err error) {
|
func (b *userBiz) FindPrivacy(loginName string) (privacy *UserPrivacy, err error) {
|
||||||
var u *dao.User
|
var u *dao.User
|
||||||
u, err = b.d.UserGetByName(context.TODO(), loginName)
|
u, err = b.d.UserGetByName(context.TODO(), loginName)
|
||||||
@ -93,6 +99,7 @@ func (b *userBiz) FindPrivacy(loginName string) (privacy *UserPrivacy, err error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *userBiz) Create(user *dao.User, ctxUser web.User) (id string, err error) {
|
func (b *userBiz) Create(user *dao.User, ctxUser web.User) (id string, err error) {
|
||||||
|
user.Tokens = data.Options{data.Option{Name: "test", Value: "abc123"}}
|
||||||
user.ID = createId()
|
user.ID = createId()
|
||||||
user.Status = UserStatusActive
|
user.Status = UserStatusActive
|
||||||
user.CreatedAt = now()
|
user.CreatedAt = now()
|
||||||
|
@ -26,6 +26,7 @@ func (d *Dao) UserUpdate(ctx context.Context, user *dao.User) (err error) {
|
|||||||
old.Admin = user.Admin
|
old.Admin = user.Admin
|
||||||
old.Type = user.Type
|
old.Type = user.Type
|
||||||
old.Roles = user.Roles
|
old.Roles = user.Roles
|
||||||
|
old.Tokens = user.Tokens
|
||||||
old.UpdatedAt = user.UpdatedAt
|
old.UpdatedAt = user.UpdatedAt
|
||||||
old.UpdatedBy = user.UpdatedBy
|
old.UpdatedBy = user.UpdatedBy
|
||||||
return old
|
return old
|
||||||
@ -101,12 +102,29 @@ func (d *Dao) UserGetByName(ctx context.Context, loginName string) (user *dao.Us
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dao) UserGetByToken(ctx context.Context, token string) (user *dao.User, err error) {
|
||||||
|
u := &dao.User{}
|
||||||
|
found, err := d.find(User, u, func() bool {
|
||||||
|
for _, t := range u.Tokens {
|
||||||
|
if t.Value == token {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if found {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Dao) UserUpdateProfile(ctx context.Context, user *dao.User) (err error) {
|
func (d *Dao) UserUpdateProfile(ctx context.Context, user *dao.User) (err error) {
|
||||||
old := &dao.User{}
|
old := &dao.User{}
|
||||||
return d.update(User, user.ID, old, func() interface{} {
|
return d.update(User, user.ID, old, func() interface{} {
|
||||||
old.Name = user.Name
|
old.Name = user.Name
|
||||||
old.LoginName = user.LoginName
|
old.LoginName = user.LoginName
|
||||||
old.Email = user.Email
|
old.Email = user.Email
|
||||||
|
old.Tokens = user.Tokens
|
||||||
old.UpdatedAt = user.UpdatedAt
|
old.UpdatedAt = user.UpdatedAt
|
||||||
old.UpdatedBy = user.UpdatedBy
|
old.UpdatedBy = user.UpdatedBy
|
||||||
return old
|
return old
|
||||||
|
@ -30,6 +30,7 @@ type Interface interface {
|
|||||||
|
|
||||||
UserGet(ctx context.Context, id string) (*User, error)
|
UserGet(ctx context.Context, id string) (*User, error)
|
||||||
UserGetByName(ctx context.Context, loginName string) (*User, error)
|
UserGetByName(ctx context.Context, loginName string) (*User, error)
|
||||||
|
UserGetByToken(ctx context.Context, token string) (user *User, err error)
|
||||||
UserSearch(ctx context.Context, args *UserSearchArgs) (users []*User, count int, err error)
|
UserSearch(ctx context.Context, args *UserSearchArgs) (users []*User, count int, err error)
|
||||||
UserCount(ctx context.Context) (int, error)
|
UserCount(ctx context.Context) (int, error)
|
||||||
UserCreate(ctx context.Context, user *User) error
|
UserCreate(ctx context.Context, user *User) error
|
||||||
|
@ -48,7 +48,7 @@ func (t Time) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Operator struct {
|
type Operator struct {
|
||||||
ID string `json:"id,omitempty" bson:"_id,omitempty"`
|
ID string `json:"id,omitempty" bson:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty" bson:"name,omitempty"`
|
Name string `json:"name,omitempty" bson:"name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,20 +72,21 @@ type Role struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id,omitempty" bson:"_id"`
|
ID string `json:"id,omitempty" bson:"_id"`
|
||||||
Name string `json:"name" bson:"name" valid:"required"`
|
Name string `json:"name" bson:"name" valid:"required"`
|
||||||
LoginName string `json:"loginName" bson:"login_name" valid:"required"`
|
LoginName string `json:"loginName" bson:"login_name" valid:"required"`
|
||||||
Password string `json:"-" bson:"password"`
|
Password string `json:"-" bson:"password"`
|
||||||
Salt string `json:"-" bson:"salt"`
|
Salt string `json:"-" bson:"salt"`
|
||||||
Email string `json:"email" bson:"email" valid:"required"`
|
Email string `json:"email" bson:"email" valid:"required"`
|
||||||
Admin bool `json:"admin" bson:"admin"`
|
Admin bool `json:"admin" bson:"admin"`
|
||||||
Type string `json:"type" bson:"type"`
|
Type string `json:"type" bson:"type"`
|
||||||
Status int32 `json:"status" bson:"status"`
|
Status int32 `json:"status" bson:"status"`
|
||||||
Roles []string `json:"roles,omitempty" bson:"roles,omitempty"`
|
Roles []string `json:"roles,omitempty" bson:"roles,omitempty"`
|
||||||
CreatedAt Time `json:"createdAt" bson:"created_at"`
|
Tokens data.Options `json:"tokens,omitempty" bson:"tokens,omitempty"`
|
||||||
UpdatedAt Time `json:"updatedAt" bson:"updated_at"`
|
CreatedAt Time `json:"createdAt" bson:"created_at"`
|
||||||
CreatedBy Operator `json:"createdBy" bson:"created_by"`
|
UpdatedAt Time `json:"updatedAt" bson:"updated_at"`
|
||||||
UpdatedBy Operator `json:"updatedBy" bson:"updated_by"`
|
CreatedBy Operator `json:"createdBy" bson:"created_by"`
|
||||||
|
UpdatedBy Operator `json:"updatedBy" bson:"updated_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSearchArgs struct {
|
type UserSearchArgs struct {
|
||||||
|
@ -24,6 +24,7 @@ var indexes = map[string][]mongo.IndexModel{
|
|||||||
Options: options.Index().SetUnique(true),
|
Options: options.Index().SetUnique(true),
|
||||||
},
|
},
|
||||||
mongo.IndexModel{Keys: bson.D{{"name", 1}}},
|
mongo.IndexModel{Keys: bson.D{{"name", 1}}},
|
||||||
|
mongo.IndexModel{Keys: bson.D{{"tokens.value", 1}}},
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
mongo.IndexModel{
|
mongo.IndexModel{
|
||||||
|
@ -28,6 +28,7 @@ func (d *Dao) UserUpdate(ctx context.Context, user *dao.User) (err error) {
|
|||||||
"admin": user.Admin,
|
"admin": user.Admin,
|
||||||
"type": user.Type,
|
"type": user.Type,
|
||||||
"roles": user.Roles,
|
"roles": user.Roles,
|
||||||
|
"tokens": user.Tokens,
|
||||||
"updated_at": user.UpdatedAt,
|
"updated_at": user.UpdatedAt,
|
||||||
"updated_by": user.UpdatedBy,
|
"updated_by": user.UpdatedBy,
|
||||||
},
|
},
|
||||||
@ -90,12 +91,24 @@ func (d *Dao) UserGetByName(ctx context.Context, loginName string) (user *dao.Us
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dao) UserGetByToken(ctx context.Context, token string) (user *dao.User, err error) {
|
||||||
|
user = &dao.User{}
|
||||||
|
err = d.db.Collection(User).FindOne(ctx, bson.M{"tokens.value": token}).Decode(user)
|
||||||
|
if err == mongo.ErrNoDocuments {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Dao) UserUpdateProfile(ctx context.Context, user *dao.User) (err error) {
|
func (d *Dao) UserUpdateProfile(ctx context.Context, user *dao.User) (err error) {
|
||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$set": bson.M{
|
"$set": bson.M{
|
||||||
"name": user.Name,
|
"name": user.Name,
|
||||||
"login_name": user.LoginName,
|
"login_name": user.LoginName,
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
|
"tokens": user.Tokens,
|
||||||
"updated_at": user.UpdatedAt,
|
"updated_at": user.UpdatedAt,
|
||||||
"updated_by": user.UpdatedBy,
|
"updated_by": user.UpdatedBy,
|
||||||
},
|
},
|
||||||
|
2
main.go
2
main.go
@ -32,7 +32,7 @@ var (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app.Name = "Swirl"
|
app.Name = "Swirl"
|
||||||
app.Version = "1.0.0beta7"
|
app.Version = "1.0.0rc1"
|
||||||
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
||||||
app.Action = func(ctx *app.Context) error {
|
app.Action = func(ctx *app.Context) error {
|
||||||
return run.Pipeline(misc.LoadOptions, initSystem, scaler.Start, startServer)
|
return run.Pipeline(misc.LoadOptions, initSystem, scaler.Start, startServer)
|
||||||
|
@ -42,7 +42,12 @@ func (c *Identifier) Apply(next web.HandlerFunc) web.HandlerFunc {
|
|||||||
return func(ctx web.Context) error {
|
return func(ctx web.Context) error {
|
||||||
token := c.extractToken(ctx)
|
token := c.extractToken(ctx)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
user := c.identifyUser(token)
|
var user web.User
|
||||||
|
if len(token) == 24 {
|
||||||
|
user = c.identifyBySession(token)
|
||||||
|
} else {
|
||||||
|
user = c.identifyByToken(token)
|
||||||
|
}
|
||||||
ctx.SetUser(user)
|
ctx.SetUser(user)
|
||||||
}
|
}
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
@ -105,7 +110,7 @@ func (c *Identifier) extractToken(ctx web.Context) (token string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Identifier) identifyUser(token string) web.User {
|
func (c *Identifier) identifyBySession(token string) web.User {
|
||||||
session, err := c.sb.Find(token)
|
session, err := c.sb.Find(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("failed to find session: ", err)
|
c.logger.Error("failed to find session: ", err)
|
||||||
@ -126,6 +131,30 @@ func (c *Identifier) identifyUser(token string) web.User {
|
|||||||
return c.createUser(session)
|
return c.createUser(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Identifier) identifyByToken(token string) web.User {
|
||||||
|
u, err := c.ub.FindByToken(token)
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Errorf("failed to find user by token '%s': %s", token, err)
|
||||||
|
return nil
|
||||||
|
} else if u == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
perms, err := c.rb.GetPerms(u.Roles)
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Error("failed to load perms: ", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &User{
|
||||||
|
token: token,
|
||||||
|
id: u.ID,
|
||||||
|
name: u.Name,
|
||||||
|
admin: u.Admin,
|
||||||
|
perm: NewPermMap(perms),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Identifier) createUser(s *dao.Session) web.User {
|
func (c *Identifier) createUser(s *dao.Session) web.User {
|
||||||
return &User{
|
return &User{
|
||||||
token: s.ID,
|
token: s.ID,
|
||||||
|
@ -50,7 +50,7 @@ var Perms = map[string][]string{
|
|||||||
"registry": {"view", "edit", "delete"},
|
"registry": {"view", "edit", "delete"},
|
||||||
"node": {"view", "edit", "delete"},
|
"node": {"view", "edit", "delete"},
|
||||||
"network": {"view", "edit", "delete", "disconnect"},
|
"network": {"view", "edit", "delete", "disconnect"},
|
||||||
"service": {"view", "edit", "delete", "restart", "rollback", "logs"},
|
"service": {"view", "edit", "delete", "deploy", "restart", "rollback", "logs"},
|
||||||
"task": {"view", "logs"},
|
"task": {"view", "logs"},
|
||||||
"stack": {"view", "edit", "delete", "deploy", "shutdown"},
|
"stack": {"view", "edit", "delete", "deploy", "shutdown"},
|
||||||
"config": {"view", "edit", "delete"},
|
"config": {"view", "edit", "delete"},
|
||||||
|
@ -17,6 +17,10 @@ export interface User {
|
|||||||
status: number;
|
status: number;
|
||||||
email: string;
|
email: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
|
tokens: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
createdBy: {
|
createdBy: {
|
||||||
|
@ -29,6 +29,7 @@ export default {
|
|||||||
"update": "Update",
|
"update": "Update",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"home": "Return to home",
|
"home": "Return to home",
|
||||||
|
"copy": "Copy",
|
||||||
},
|
},
|
||||||
"perms": {
|
"perms": {
|
||||||
"view": "View",
|
"view": "View",
|
||||||
@ -182,6 +183,7 @@ export default {
|
|||||||
"admins": "Admins",
|
"admins": "Admins",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
|
"tokens": "Tokens",
|
||||||
"perms": "Permissions",
|
"perms": "Permissions",
|
||||||
"margin": "Margin",
|
"margin": "Margin",
|
||||||
"metrics": "Metrics",
|
"metrics": "Metrics",
|
||||||
@ -372,6 +374,7 @@ export default {
|
|||||||
"profile": "User personal information",
|
"profile": "User personal information",
|
||||||
"password": "User login password",
|
"password": "User login password",
|
||||||
"preference": "User personalization",
|
"preference": "User personalization",
|
||||||
|
"copied": "Copied",
|
||||||
"required_rule": "Cannot be empty",
|
"required_rule": "Cannot be empty",
|
||||||
"email_rule": "Incorrect email format",
|
"email_rule": "Incorrect email format",
|
||||||
"length_rule": "The length must be {min}-{max} bits",
|
"length_rule": "The length must be {min}-{max} bits",
|
||||||
|
@ -29,6 +29,7 @@ export default {
|
|||||||
"update": "更新",
|
"update": "更新",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"home": "返回首页",
|
"home": "返回首页",
|
||||||
|
"copy": "复制",
|
||||||
},
|
},
|
||||||
"perms": {
|
"perms": {
|
||||||
"view": "浏览",
|
"view": "浏览",
|
||||||
@ -182,6 +183,7 @@ export default {
|
|||||||
"admins": "@:fields.admin",
|
"admins": "@:fields.admin",
|
||||||
"active": "正常",
|
"active": "正常",
|
||||||
"blocked": "屏蔽",
|
"blocked": "屏蔽",
|
||||||
|
"tokens": "凭证",
|
||||||
"perms": "权限",
|
"perms": "权限",
|
||||||
"margin": "边距",
|
"margin": "边距",
|
||||||
"metrics": "指标",
|
"metrics": "指标",
|
||||||
@ -372,6 +374,7 @@ export default {
|
|||||||
"profile": "用户个人资料",
|
"profile": "用户个人资料",
|
||||||
"password": "用户登录密码",
|
"password": "用户登录密码",
|
||||||
"preference": "用户个性化设置",
|
"preference": "用户个性化设置",
|
||||||
|
"copied": "已复制",
|
||||||
"required_rule": "不能为空",
|
"required_rule": "不能为空",
|
||||||
"email_rule": "电子邮箱格式不正确",
|
"email_rule": "电子邮箱格式不正确",
|
||||||
"length_rule": "长度必须为 {min}-{max} 位",
|
"length_rule": "长度必须为 {min}-{max} 位",
|
||||||
|
@ -28,6 +28,38 @@
|
|||||||
<n-form-item-gi :label="t('fields.email')" path="email">
|
<n-form-item-gi :label="t('fields.email')" path="email">
|
||||||
<n-input :placeholder="t('fields.email')" v-model:value="profile.email" />
|
<n-input :placeholder="t('fields.email')" v-model:value="profile.email" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="t('fields.tokens')" path="tokens" span="2">
|
||||||
|
<n-dynamic-input
|
||||||
|
v-model:value="profile.tokens"
|
||||||
|
#="{ index, value }"
|
||||||
|
:on-create="() => ({ name: '', value: guid() })"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
:placeholder="t('fields.name')"
|
||||||
|
v-model:value="value.name"
|
||||||
|
style="width: 300px"
|
||||||
|
/>
|
||||||
|
<div style="height: 34px; line-height: 34px; margin: 0 8px">=</div>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input :placeholder="t('fields.value')" v-model:value="value.value" readonly></n-input>
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
type="default"
|
||||||
|
#icon
|
||||||
|
@click="() => copy(value.value)"
|
||||||
|
v-if="isSupported"
|
||||||
|
>
|
||||||
|
<n-icon>
|
||||||
|
<copy-icon />
|
||||||
|
</n-icon>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t(copied ? 'tips.copied' : 'buttons.copy') }}
|
||||||
|
</n-tooltip>
|
||||||
|
</n-input-group>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</n-form>
|
</n-form>
|
||||||
<n-button
|
<n-button
|
||||||
@ -153,6 +185,7 @@ import {
|
|||||||
NButton,
|
NButton,
|
||||||
NSpace,
|
NSpace,
|
||||||
NInput,
|
NInput,
|
||||||
|
NInputGroup,
|
||||||
NIcon,
|
NIcon,
|
||||||
NForm,
|
NForm,
|
||||||
NFormItem,
|
NFormItem,
|
||||||
@ -161,9 +194,12 @@ import {
|
|||||||
NRadioButton,
|
NRadioButton,
|
||||||
NRadioGroup,
|
NRadioGroup,
|
||||||
NAlert,
|
NAlert,
|
||||||
|
NDynamicInput,
|
||||||
|
NTooltip,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import {
|
import {
|
||||||
SaveOutline as SaveIcon,
|
SaveOutline as SaveIcon,
|
||||||
|
CopyOutline as CopyIcon,
|
||||||
} from "@vicons/ionicons5";
|
} from "@vicons/ionicons5";
|
||||||
import XPageHeader from "@/components/PageHeader.vue";
|
import XPageHeader from "@/components/PageHeader.vue";
|
||||||
import XPanel from "@/components/Panel.vue";
|
import XPanel from "@/components/Panel.vue";
|
||||||
@ -173,6 +209,8 @@ import { useForm, emailRule, requiredRule, customRule, lengthRule } from "@/util
|
|||||||
import { Mutations } from "@/store/mutations";
|
import { Mutations } from "@/store/mutations";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
import { guid } from "@/utils";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const panel = ref('')
|
const panel = ref('')
|
||||||
@ -193,6 +231,7 @@ const profileRules: any = {
|
|||||||
};
|
};
|
||||||
const profileForm = ref();
|
const profileForm = ref();
|
||||||
const { submit: modifyProfile, submiting: profileSubmiting } = useForm(profileForm, () => userApi.modifyProfile(profile.value))
|
const { submit: modifyProfile, submiting: profileSubmiting } = useForm(profileForm, () => userApi.modifyProfile(profile.value))
|
||||||
|
const { copy, copied, isSupported } = useClipboard()
|
||||||
|
|
||||||
// password
|
// password
|
||||||
const password = reactive({
|
const password = reactive({
|
||||||
|
@ -1,39 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<x-page-header />
|
<x-page-header />
|
||||||
<n-space class="page-body" vertical :size="12">
|
<n-space class="page-body" vertical :size="12">
|
||||||
<x-panel title="Deployment" divider="bottom" :collapsed="panel !== 'deploy'" v-if="false">
|
|
||||||
<template #action>
|
|
||||||
<n-button
|
|
||||||
secondary
|
|
||||||
strong
|
|
||||||
class="toggle"
|
|
||||||
size="small"
|
|
||||||
@click="togglePanel('deploy')"
|
|
||||||
>{{ panel === 'deploy' ? t('buttons.collapse') : t('buttons.expand') }}</n-button>
|
|
||||||
</template>
|
|
||||||
<div style="padding: 4px 0 0 12px">
|
|
||||||
<n-form :model="setting" ref="formDeploy" :show-feedback="false">
|
|
||||||
<n-form-item :label="t('fields.keys')" path="deploy.keys">
|
|
||||||
<n-dynamic-input
|
|
||||||
v-model:value="setting.deploy.keys"
|
|
||||||
#="{ index, value }"
|
|
||||||
:on-create="newKey"
|
|
||||||
>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input :placeholder="t('fields.name')" v-model:value="value.name" />
|
|
||||||
<n-input :placeholder="t('fields.token')" v-model:value="value.token" />
|
|
||||||
<n-date-picker :placeholder="t('fields.expiry')" v-model:value="value.expiry" type="date" clearable style="min-width: 200px"/>
|
|
||||||
</n-input-group>
|
|
||||||
</n-dynamic-input>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
style="margin-top: 12px"
|
|
||||||
@click="() => save('deploy', setting.deploy)"
|
|
||||||
>{{ t('buttons.save') }}</n-button>
|
|
||||||
</div>
|
|
||||||
</x-panel>
|
|
||||||
<x-panel title="LDAP" :subtitle="t('tips.ldap')" divider="bottom" :collapsed="panel !== 'ldap'">
|
<x-panel title="LDAP" :subtitle="t('tips.ldap')" divider="bottom" :collapsed="panel !== 'ldap'">
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button
|
<n-button
|
||||||
@ -181,10 +148,8 @@ import {
|
|||||||
NFormItemGi,
|
NFormItemGi,
|
||||||
NRadioGroup,
|
NRadioGroup,
|
||||||
NRadio,
|
NRadio,
|
||||||
NDynamicInput,
|
|
||||||
NSwitch,
|
NSwitch,
|
||||||
NAlert,
|
NAlert,
|
||||||
NDatePicker,
|
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import XPageHeader from "@/components/PageHeader.vue";
|
import XPageHeader from "@/components/PageHeader.vue";
|
||||||
import XPanel from "@/components/Panel.vue";
|
import XPanel from "@/components/Panel.vue";
|
||||||
@ -211,10 +176,6 @@ function togglePanel(name: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function newKey() {
|
|
||||||
return { name: '', token: '', expiry: undefined }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save(id: string, options: any) {
|
async function save(id: string, options: any) {
|
||||||
await settingApi.save(id, options)
|
await settingApi.save(id, options)
|
||||||
window.message.info(t('texts.action_success'));
|
window.message.info(t('texts.action_success'));
|
||||||
|
@ -71,6 +71,38 @@
|
|||||||
</n-space>
|
</n-space>
|
||||||
</n-checkbox-group>
|
</n-checkbox-group>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="t('fields.tokens', 2)" span="2" path="tokens">
|
||||||
|
<n-dynamic-input
|
||||||
|
v-model:value="user.tokens"
|
||||||
|
#="{ index, value }"
|
||||||
|
:on-create="() => ({ name: '', value: guid() })"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
:placeholder="t('fields.name')"
|
||||||
|
v-model:value="value.name"
|
||||||
|
style="width: 300px"
|
||||||
|
/>
|
||||||
|
<div style="height: 34px; line-height: 34px; margin: 0 8px">=</div>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input :placeholder="t('fields.value')" v-model:value="value.value" readonly></n-input>
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
type="default"
|
||||||
|
#icon
|
||||||
|
@click="() => copy(value.value)"
|
||||||
|
v-if="isSupported"
|
||||||
|
>
|
||||||
|
<n-icon>
|
||||||
|
<copy-icon />
|
||||||
|
</n-icon>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t(copied ? 'tips.copied' : 'buttons.copy') }}
|
||||||
|
</n-tooltip>
|
||||||
|
</n-input-group>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item-gi>
|
||||||
<n-gi :span="2">
|
<n-gi :span="2">
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="submiting"
|
:disabled="submiting"
|
||||||
@ -97,6 +129,7 @@ import {
|
|||||||
NButton,
|
NButton,
|
||||||
NSpace,
|
NSpace,
|
||||||
NInput,
|
NInput,
|
||||||
|
NInputGroup,
|
||||||
NIcon,
|
NIcon,
|
||||||
NForm,
|
NForm,
|
||||||
NGrid,
|
NGrid,
|
||||||
@ -107,10 +140,13 @@ import {
|
|||||||
NCheckbox,
|
NCheckbox,
|
||||||
NRadioGroup,
|
NRadioGroup,
|
||||||
NRadio,
|
NRadio,
|
||||||
|
NDynamicInput,
|
||||||
|
NTooltip,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import {
|
import {
|
||||||
ArrowBackCircleOutline as BackIcon,
|
ArrowBackCircleOutline as BackIcon,
|
||||||
SaveOutline as SaveIcon,
|
SaveOutline as SaveIcon,
|
||||||
|
CopyOutline as CopyIcon,
|
||||||
} from "@vicons/ionicons5";
|
} from "@vicons/ionicons5";
|
||||||
import XPageHeader from "@/components/PageHeader.vue";
|
import XPageHeader from "@/components/PageHeader.vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
@ -119,8 +155,10 @@ import userApi from "@/api/user";
|
|||||||
import roleApi from "@/api/role";
|
import roleApi from "@/api/role";
|
||||||
import type { User } from "@/api/user";
|
import type { User } from "@/api/user";
|
||||||
import type { Role } from "@/api/role";
|
import type { Role } from "@/api/role";
|
||||||
import { useForm, emailRule, requiredRule } from "@/utils/form";
|
import { useForm, emailRule, requiredRule, customRule } from "@/utils/form";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
import { guid } from "@/utils";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -132,12 +170,16 @@ const rules: any = {
|
|||||||
email: [requiredRule(), emailRule()],
|
email: [requiredRule(), emailRule()],
|
||||||
password: requiredRule(),
|
password: requiredRule(),
|
||||||
passwordConfirm: requiredRule(),
|
passwordConfirm: requiredRule(),
|
||||||
|
tokens: customRule((rule: any, value: any[]) => {
|
||||||
|
return value?.every(v => v.name && v.value)
|
||||||
|
}, t('tips.required_rule')),
|
||||||
};
|
};
|
||||||
const form = ref();
|
const form = ref();
|
||||||
const { submit, submiting } = useForm(form, () => userApi.save(user.value), () => {
|
const { submit, submiting } = useForm(form, () => userApi.save(user.value), () => {
|
||||||
window.message.info(t('texts.action_success'));
|
window.message.info(t('texts.action_success'));
|
||||||
router.push({ name: 'user_list' })
|
router.push({ name: 'user_list' })
|
||||||
})
|
})
|
||||||
|
const { copy, copied, isSupported } = useClipboard()
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const id = route.params.id as string || ''
|
const id = route.params.id as string || ''
|
||||||
|
@ -46,4 +46,12 @@ export function isEmpty(...arrs: (any[] | undefined)[]): boolean {
|
|||||||
|
|
||||||
export function toTitle(s: string): string {
|
export function toTitle(s: string): string {
|
||||||
return s ? s[0].toUpperCase() + s.substring(1) : s
|
return s ? s[0].toUpperCase() + s.substring(1) : s
|
||||||
|
}
|
||||||
|
|
||||||
|
export function guid() {
|
||||||
|
return s4() + s4() + s4() + s4() + s4() + s4() + s4() + s4()
|
||||||
|
}
|
||||||
|
|
||||||
|
function s4() {
|
||||||
|
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||||
}
|
}
|
@ -13,7 +13,7 @@ export const perms = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'service',
|
key: 'service',
|
||||||
actions: ['view', 'edit', 'delete', 'restart', 'rollback', 'logs'],
|
actions: ['view', 'edit', 'delete', 'deploy', 'restart', 'rollback', 'logs'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'task',
|
key: 'task',
|
||||||
|
Loading…
Reference in New Issue
Block a user