diff --git a/api/init.go b/api/init.go index e0cd0ea..012f5d5 100644 --- a/api/init.go +++ b/api/init.go @@ -11,6 +11,7 @@ import ( apiAuth "github.com/veypi/vbase/api/auth" "github.com/veypi/vbase/api/oauth" "github.com/veypi/vbase/api/org" + "github.com/veypi/vbase/api/role" "github.com/veypi/vbase/api/user" "github.com/veypi/vbase/auth" "github.com/veypi/vigo" @@ -28,6 +29,7 @@ func init() { Router.Extend("/auth", apiAuth.Router) Router.Extend("/users", user.Router) Router.Extend("/orgs", org.Router) + Router.Extend("/roles", role.Router) Router.Extend("/oauth", oauth.Router) // 404 处理 diff --git a/api/role/create.go b/api/role/create.go new file mode 100644 index 0000000..d151a98 --- /dev/null +++ b/api/role/create.go @@ -0,0 +1,43 @@ +package role + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" +) + +type CreateReq struct { + Code string `json:"code" src:"json" desc:"Role Code"` + Name string `json:"name" src:"json" desc:"Role Name"` + Description string `json:"description" src:"json" desc:"Role Description"` + OrgID string `json:"org_id" src:"json" desc:"Organization ID (Optional)"` +} + +func create(x *vigo.X, req *CreateReq) (*models.Role, error) { + // Check if role code already exists + var count int64 + if err := cfg.DB().Model(&models.Role{}).Where("code = ?", req.Code).Count(&count).Error; err != nil { + return nil, vigo.ErrInternalServer.WithError(err) + } + if count > 0 { + return nil, vigo.ErrAlreadyExists.WithArgs("Role Code") + } + + role := &models.Role{ + Code: req.Code, + Name: req.Name, + Description: req.Description, + IsSystem: false, // Default to false for user created roles + Status: 1, + } + + if req.OrgID != "" { + role.OrgID = &req.OrgID + } + + if err := cfg.DB().Create(role).Error; err != nil { + return nil, vigo.ErrDatabase.WithError(err) + } + + return role, nil +} diff --git a/api/role/del.go b/api/role/del.go new file mode 100644 index 0000000..0c6c4f7 --- /dev/null +++ b/api/role/del.go @@ -0,0 +1,28 @@ +package role + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" +) + +type DelReq struct { + ID string `src:"path@id" desc:"Role ID"` +} + +func del(x *vigo.X, req *DelReq) error { + var role models.Role + if err := cfg.DB().First(&role, "id = ?", req.ID).Error; err != nil { + return vigo.ErrNotFound + } + + if role.IsSystem { + return vigo.NewError("cannot delete system role").WithCode(40300) + } + + if err := cfg.DB().Delete(&role).Error; err != nil { + return vigo.ErrDatabase.WithError(err) + } + + return nil +} diff --git a/api/role/get.go b/api/role/get.go new file mode 100644 index 0000000..b4bd316 --- /dev/null +++ b/api/role/get.go @@ -0,0 +1,19 @@ +package role + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" +) + +type GetReq struct { + ID string `src:"path@id" desc:"Role ID"` +} + +func get(x *vigo.X, req *GetReq) (*models.Role, error) { + var role models.Role + if err := cfg.DB().First(&role, "id = ?", req.ID).Error; err != nil { + return nil, vigo.ErrNotFound + } + return &role, nil +} diff --git a/api/role/init.go b/api/role/init.go new file mode 100644 index 0000000..4c51cd4 --- /dev/null +++ b/api/role/init.go @@ -0,0 +1,18 @@ +package role + +import ( + "github.com/veypi/vbase/auth" + "github.com/veypi/vigo" +) + +var Router = vigo.NewRouter() + +func init() { + Router.Get("/", "List Roles", auth.VBaseAuth.Perm("role:read"), list) + Router.Get("/{id}", "Get Role Detail", auth.VBaseAuth.Perm("role:read"), get) + Router.Post("/", "Create Role", auth.VBaseAuth.Perm("role:create"), create) + Router.Patch("/{id}", "Update Role", auth.VBaseAuth.Perm("role:update"), patch) + Router.Delete("/{id}", "Delete Role", auth.VBaseAuth.Perm("role:delete"), del) + Router.Get("/{id}/permissions", "Get Role Permissions", auth.VBaseAuth.Perm("role:read"), getPermissions) + Router.Put("/{id}/permissions", "Update Role Permissions", auth.VBaseAuth.Perm("role:update"), updatePermissions) +} diff --git a/api/role/list.go b/api/role/list.go new file mode 100644 index 0000000..0447885 --- /dev/null +++ b/api/role/list.go @@ -0,0 +1,58 @@ +package role + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" +) + +type ListReq struct { + Page int `json:"page" src:"query" default:"1"` + PageSize int `json:"page_size" src:"query" default:"20"` + OrgID *string `json:"org_id" src:"query" desc:"Organization ID"` + Keyword *string `json:"keyword" src:"query" desc:"Search Keyword"` +} + +type ListResp struct { + Items []models.Role `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +func list(x *vigo.X, req *ListReq) (*ListResp, error) { + db := cfg.DB().Model(&models.Role{}) + + if req.OrgID != nil { + db = db.Where("org_id = ?", *req.OrgID) + } + + if req.Keyword != nil && *req.Keyword != "" { + db = db.Where("name LIKE ? OR code LIKE ?", "%"+*req.Keyword+"%", "%"+*req.Keyword+"%") + } + + var total int64 + if err := db.Count(&total).Error; err != nil { + return nil, vigo.ErrInternalServer.WithError(err) + } + + var roles []models.Role + offset := (req.Page - 1) * req.PageSize + if err := db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&roles).Error; err != nil { + return nil, vigo.ErrInternalServer.WithError(err) + } + + totalPages := int(total) / req.PageSize + if int(total)%req.PageSize > 0 { + totalPages++ + } + + return &ListResp{ + Items: roles, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + TotalPages: totalPages, + }, nil +} diff --git a/api/role/patch.go b/api/role/patch.go new file mode 100644 index 0000000..df7166a --- /dev/null +++ b/api/role/patch.go @@ -0,0 +1,44 @@ +package role + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" +) + +type PatchReq struct { + ID string `src:"path@id" desc:"Role ID"` + Name *string `json:"name" src:"json" desc:"Role Name"` + Description *string `json:"description" src:"json" desc:"Role Description"` + Status *int `json:"status" src:"json" desc:"Status"` +} + +func patch(x *vigo.X, req *PatchReq) (*models.Role, error) { + var role models.Role + if err := cfg.DB().First(&role, "id = ?", req.ID).Error; err != nil { + return nil, vigo.ErrNotFound + } + + if role.IsSystem { + return nil, vigo.NewError("cannot modify system role").WithCode(40300) + } + + updates := map[string]interface{}{} + if req.Name != nil { + updates["name"] = *req.Name + } + if req.Description != nil { + updates["description"] = *req.Description + } + if req.Status != nil { + updates["status"] = *req.Status + } + + if len(updates) > 0 { + if err := cfg.DB().Model(&role).Updates(updates).Error; err != nil { + return nil, vigo.ErrDatabase.WithError(err) + } + } + + return &role, nil +} diff --git a/api/role/permissions.go b/api/role/permissions.go new file mode 100644 index 0000000..98b8661 --- /dev/null +++ b/api/role/permissions.go @@ -0,0 +1,64 @@ +package role + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" + "gorm.io/gorm" +) + +type GetPermissionsReq struct { + RoleID string `src:"path@id" desc:"Role ID"` +} + +func getPermissions(x *vigo.X, req *GetPermissionsReq) ([]models.Permission, error) { + var rolePermissions []models.RolePermission + if err := cfg.DB().Preload("Permission").Where("role_id = ?", req.RoleID).Find(&rolePermissions).Error; err != nil { + return nil, vigo.ErrDatabase.WithError(err) + } + + permissions := make([]models.Permission, 0, len(rolePermissions)) + for _, rp := range rolePermissions { + permissions = append(permissions, rp.Permission) + } + return permissions, nil +} + +type UpdatePermissionsReq struct { + RoleID string `src:"path@id" desc:"Role ID"` + PermissionIDs []string `json:"permission_ids" src:"json" desc:"List of Permission IDs"` +} + +func updatePermissions(x *vigo.X, req *UpdatePermissionsReq) error { + var role models.Role + if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil { + return vigo.ErrNotFound + } + + if role.IsSystem { + return vigo.NewError("cannot modify permissions of system role").WithCode(40300) + } + + return cfg.DB().Transaction(func(tx *gorm.DB) error { + // Delete existing permissions + if err := tx.Where("role_id = ?", req.RoleID).Delete(&models.RolePermission{}).Error; err != nil { + return err + } + + // Add new permissions + if len(req.PermissionIDs) > 0 { + rolePermissions := make([]models.RolePermission, 0, len(req.PermissionIDs)) + for _, pid := range req.PermissionIDs { + rolePermissions = append(rolePermissions, models.RolePermission{ + RoleID: req.RoleID, + PermissionID: pid, + Condition: "none", // Default condition + }) + } + if err := tx.Create(&rolePermissions).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/api/user/init.go b/api/user/init.go index b341a31..adf0f70 100644 --- a/api/user/init.go +++ b/api/user/init.go @@ -20,4 +20,9 @@ func init() { Router.Patch("/{user_id}", "更新用户", auth.VBaseAuth.PermWithOwner("user:update", "user_id"), patch) Router.Delete("/{user_id}", "删除用户", auth.VBaseAuth.Perm("user:admin"), del) Router.Patch("/{user_id}/status", "更新用户状态", auth.VBaseAuth.Perm("user:admin"), updateStatus) + + Router.Get("/{user_id}/roles", "Get User Roles", auth.VBaseAuth.PermWithOwner("user:read", "user_id"), getRoles) + Router.Put("/{user_id}/roles", "Update User Roles", auth.VBaseAuth.Perm("user:admin"), updateRoles) + Router.Get("/{user_id}/permissions", "Get User Permissions", auth.VBaseAuth.PermWithOwner("user:read", "user_id"), getPermissions) + Router.Put("/{user_id}/permissions", "Update User Permissions", auth.VBaseAuth.Perm("user:admin"), updatePermissions) } diff --git a/api/user/permissions.go b/api/user/permissions.go new file mode 100644 index 0000000..e5ffb35 --- /dev/null +++ b/api/user/permissions.go @@ -0,0 +1,66 @@ +package user + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" + "gorm.io/gorm" +) + +// User specific permissions (data-level or direct assignment) + +type GetPermissionsReq struct { + UserID string `src:"path@user_id" desc:"User ID"` +} + +func getPermissions(x *vigo.X, req *GetPermissionsReq) ([]models.UserPermission, error) { + var userPermissions []models.UserPermission + if err := cfg.DB().Where("user_id = ?", req.UserID).Find(&userPermissions).Error; err != nil { + return nil, vigo.ErrDatabase.WithError(err) + } + return userPermissions, nil +} + +type UpdatePermissionsReq struct { + UserID string `src:"path@user_id" desc:"User ID"` + Permissions []struct { + PermissionID string `json:"permission_id"` + ResourceID string `json:"resource_id"` + } `json:"permissions" src:"json" desc:"List of User Permissions"` +} + +func updatePermissions(x *vigo.X, req *UpdatePermissionsReq) error { + var user models.User + if err := cfg.DB().First(&user, "id = ?", req.UserID).Error; err != nil { + return vigo.ErrNotFound + } + + grantor := "" + if uid := x.Get("user_id"); uid != nil { + if s, ok := uid.(string); ok { + grantor = s + } + } + + return cfg.DB().Transaction(func(tx *gorm.DB) error { + if err := tx.Where("user_id = ?", req.UserID).Delete(&models.UserPermission{}).Error; err != nil { + return err + } + + if len(req.Permissions) > 0 { + userPermissions := make([]models.UserPermission, 0, len(req.Permissions)) + for _, p := range req.Permissions { + userPermissions = append(userPermissions, models.UserPermission{ + UserID: req.UserID, + PermissionID: p.PermissionID, + ResourceID: p.ResourceID, + GrantedBy: grantor, + }) + } + if err := tx.Create(&userPermissions).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/api/user/roles.go b/api/user/roles.go new file mode 100644 index 0000000..b16d4b8 --- /dev/null +++ b/api/user/roles.go @@ -0,0 +1,57 @@ +package user + +import ( + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" + "github.com/veypi/vigo" + "gorm.io/gorm" +) + +type GetRolesReq struct { + UserID string `src:"path@user_id" desc:"User ID"` +} + +func getRoles(x *vigo.X, req *GetRolesReq) ([]models.Role, error) { + var userRoles []models.UserRole + if err := cfg.DB().Preload("Role").Where("user_id = ?", req.UserID).Find(&userRoles).Error; err != nil { + return nil, vigo.ErrDatabase.WithError(err) + } + + roles := make([]models.Role, 0, len(userRoles)) + for _, ur := range userRoles { + roles = append(roles, ur.Role) + } + return roles, nil +} + +type UpdateRolesReq struct { + UserID string `src:"path@user_id" desc:"User ID"` + RoleIDs []string `json:"role_ids" src:"json" desc:"Role IDs"` +} + +func updateRoles(x *vigo.X, req *UpdateRolesReq) error { + var user models.User + if err := cfg.DB().First(&user, "id = ?", req.UserID).Error; err != nil { + return vigo.ErrNotFound + } + + return cfg.DB().Transaction(func(tx *gorm.DB) error { + if err := tx.Where("user_id = ?", req.UserID).Delete(&models.UserRole{}).Error; err != nil { + return err + } + + if len(req.RoleIDs) > 0 { + userRoles := make([]models.UserRole, 0, len(req.RoleIDs)) + for _, rid := range req.RoleIDs { + userRoles = append(userRoles, models.UserRole{ + UserID: req.UserID, + RoleID: rid, + }) + } + if err := tx.Create(&userRoles).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/auth/auth.go b/auth/auth.go index 0a3975d..5d75d4f 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -668,7 +668,6 @@ func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissi Count(&count).Error; err != nil { return false, err } - fmt.Printf("[DEBUG] CheckPermission: role perm count=%d checked=%v\n", count, permsToCheck) if count > 0 { return true, nil } diff --git a/ui/langs.json b/ui/langs.json index a49e479..e37bfcc 100644 --- a/ui/langs.json +++ b/ui/langs.json @@ -21,6 +21,7 @@ "nav.home": "Home", "nav.oauth": "OAuth Apps", "nav.org": "Organizations", + "nav.roles": "Roles", "nav.profile": "Profile", "nav.users": "Users", "org.create": "Create Organization", @@ -53,7 +54,14 @@ "user.email": "Email", "user.profile": "User Profile", "user.role": "Role", - "user.username": "Username" + "user.username": "Username", + "role.name": "Role Name", + "role.code": "Role Code", + "role.description": "Description", + "role.create": "Create Role", + "role.edit": "Edit Role", + "role.delete_confirm": "Are you sure you want to delete this role?", + "role.search_placeholder": "Search roles..." }, "zh-CN": { "auth.email": "邮箱", @@ -77,6 +85,7 @@ "nav.home": "首页", "nav.oauth": "OAuth应用", "nav.org": "组织管理", + "nav.roles": "角色管理", "nav.profile": "个人中心", "nav.users": "用户管理", "org.create": "创建组织", @@ -109,6 +118,13 @@ "user.email": "邮箱", "user.profile": "个人资料", "user.role": "角色", - "user.username": "用户名" + "user.username": "用户名", + "role.name": "角色名称", + "role.code": "角色代码", + "role.description": "描述", + "role.create": "创建角色", + "role.edit": "编辑角色", + "role.delete_confirm": "确定要删除该角色吗?", + "role.search_placeholder": "搜索角色..." } } \ No newline at end of file diff --git a/ui/layout/default.html b/ui/layout/default.html index 5679c2c..e627422 100644 --- a/ui/layout/default.html +++ b/ui/layout/default.html @@ -162,6 +162,7 @@ {label: () => $t('nav.profile'), icon: "", path: "/profile"}, // Admin only items would be filtered here ideally {label: () => $t('nav.users'), icon: "", path: "/users"}, + {label: () => $t('nav.roles'), icon: "", path: "/roles"}, {label: () => $t('nav.oauth'), icon: "", path: "/oauth/apps"}, ]; diff --git a/ui/page/sys/oauth/index.html b/ui/page/sys/oauth/index.html index 2d838a7..f9f5925 100644 --- a/ui/page/sys/oauth/index.html +++ b/ui/page/sys/oauth/index.html @@ -174,7 +174,7 @@ loadApps = async () => { try { const res = await $axios.get('/api/oauth/clients'); - apps = res || []; + apps = res.items || []; } catch (e) { $message.error(e.message); } diff --git a/ui/page/sys/org/detail.html b/ui/page/sys/org/detail.html index 17528b0..31af215 100644 --- a/ui/page/sys/org/detail.html +++ b/ui/page/sys/org/detail.html @@ -4,225 +4,225 @@
{{ $t('org.no_members') }}
| {{ $t('user.username') }} | @@ -303,7 +303,7 @@|||
|---|---|---|---|
| {{ member.username }} | {{ member.email || '-' }} | @@ -313,11 +313,11 @@ |
|
@@ -325,15 +325,15 @@
| ID | +{{ $t('role.code') }} | +{{ $t('role.name') }} | +{{ $t('role.description') }} | +System | +Status | +{{ $t('common.actions') }} | +
|---|---|---|---|---|---|---|
| {{ r.id }} | +{{ r.code }} | +{{ r.name }} | +{{ r.description }} | ++ + {{ r.is_system ? 'Yes' : 'No' }} + + | ++ + {{ r.status === 1 ? 'Active' : 'Inactive' }} + + | +
+
+
+ |
+