diff --git a/api/auth/me.go b/api/auth/me.go index 169a568..d4cc8cf 100644 --- a/api/auth/me.go +++ b/api/auth/me.go @@ -16,8 +16,10 @@ import ( // UserPermissionInfo 用户权限信息 type UserPermissionInfo struct { + Scope string `json:"scope"` PermissionID string `json:"permission_id"` ResourceID string `json:"resource_id"` + Level int `json:"level"` } // UserInfoWithPerms 带权限的用户信息 @@ -45,16 +47,31 @@ func me(x *vigo.X) (*UserInfoWithPerms, error) { // 获取用户权限列表 var perms []models.Permission + // 1. 获取用户直接拥有的权限 if err := cfg.DB().Where("user_id = ?", userID).Find(&perms).Error; err != nil { return nil, vigo.ErrInternalServer.WithError(err) } + // 2. 获取用户角色的权限 + var roleIDs []string + cfg.DB().Model(&models.UserRole{}).Where("user_id = ?", userID).Pluck("role_id", &roleIDs) + + if len(roleIDs) > 0 { + var rolePerms []models.Permission + if err := cfg.DB().Where("role_id IN ?", roleIDs).Find(&rolePerms).Error; err != nil { + return nil, vigo.ErrInternalServer.WithError(err) + } + perms = append(perms, rolePerms...) + } + // 转换权限格式 userPerms := make([]UserPermissionInfo, 0, len(perms)) for _, p := range perms { userPerms = append(userPerms, UserPermissionInfo{ + Scope: p.Scope, PermissionID: p.PermissionID, ResourceID: "", // ResourceID is no longer a separate field in Permission model, using PermissionID hierarchy + Level: p.Level, }) } diff --git a/api/auth/register.go b/api/auth/register.go index 820b03d..06a5bae 100644 --- a/api/auth/register.go +++ b/api/auth/register.go @@ -155,7 +155,7 @@ func register(x *vigo.X, req *RegisterRequest) (*AuthResponse, error) { // 记录错误但允许注册继续,或者回滚 // 这里简单处理,继续流程,用户可能需要管理员手动授权 // 或者返回错误 - // return nil, vigo.ErrInternalServer.WithError(err) + return nil, vigo.ErrInternalServer.WithError(err) } // 生成token diff --git a/api/init.go b/api/init.go index fd529ef..3bf8f4c 100644 --- a/api/init.go +++ b/api/init.go @@ -24,21 +24,21 @@ var Router = vigo.NewRouter() // PublicInfoResponse 公开信息响应 // 不需要登录即可访问,用于前端初始化 - type PublicInfoResponse struct { - AppName string `json:"app_name"` - AppID string `json:"app_id"` - OAuthProviders []OAuthProviderInfo `json:"oauth_providers"` - LoginMethods []string `json:"login_methods"` - PasswordFields []string `json:"password_fields"` - RegRequireEmail bool `json:"reg_require_email"` - RegRequirePhone bool `json:"reg_require_phone"` - CaptchaEnabled bool `json:"captcha_enabled"` - EmailEnabled bool `json:"email_enabled"` - SMSEnabled bool `json:"sms_enabled"` +type PublicInfoResponse struct { + AppName string `json:"app_name"` + AppID string `json:"app_id"` + OAuthProviders []OAuthProviderInfo `json:"oauth_providers"` + LoginMethods []string `json:"login_methods"` + PasswordFields []string `json:"password_fields"` + RegRequireEmail bool `json:"reg_require_email"` + RegRequirePhone bool `json:"reg_require_phone"` + CaptchaEnabled bool `json:"captcha_enabled"` + EmailEnabled bool `json:"email_enabled"` + SMSEnabled bool `json:"sms_enabled"` } // OAuthProviderInfo OAuth提供商公开信息 - type OAuthProviderInfo struct { +type OAuthProviderInfo struct { Code string `json:"code"` Name string `json:"name"` Icon string `json:"icon"` @@ -72,7 +72,7 @@ func init() { } // getPublicInfo 获取公开配置信息 - func getPublicInfo(x *vigo.X) (*PublicInfoResponse, error) { +func getPublicInfo(x *vigo.X) (*PublicInfoResponse, error) { resp := &PublicInfoResponse{} // 应用配置 diff --git a/api/role/create.go b/api/role/create.go index a366b9a..6b66a8f 100644 --- a/api/role/create.go +++ b/api/role/create.go @@ -16,7 +16,7 @@ type CreateReq struct { 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 = ? AND scope = ?", req.Code, req.Scope).Count(&count).Error; err != nil { + 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 { @@ -24,7 +24,6 @@ func create(x *vigo.X, req *CreateReq) (*models.Role, error) { } role := &models.Role{ - Scope: req.Scope, Code: req.Code, Name: req.Name, IsSystem: false, // Default to false for user created roles diff --git a/api/role/patch.go b/api/role/patch.go index 8068004..3bf60cc 100644 --- a/api/role/patch.go +++ b/api/role/patch.go @@ -27,28 +27,20 @@ func patch(x *vigo.X, req *PatchReq) (*models.Role, error) { updates := map[string]interface{}{} - // Check if code or scope is being updated - if req.Code != nil || req.Scope != nil { - newCode := role.Code - if req.Code != nil { - newCode = *req.Code - } - newScope := role.Scope - if req.Scope != nil { - newScope = *req.Scope - } + // Check if code is being updated + if req.Code != nil { + newCode := *req.Code // Check for uniqueness if changed - if newCode != role.Code || newScope != role.Scope { + if newCode != role.Code { var count int64 - if err := cfg.DB().Model(&models.Role{}).Where("code = ? AND scope = ? AND id != ?", newCode, newScope, role.ID).Count(&count).Error; err != nil { + if err := cfg.DB().Model(&models.Role{}).Where("code = ? AND id != ?", newCode, role.ID).Count(&count).Error; err != nil { return nil, vigo.ErrInternalServer.WithError(err) } if count > 0 { - return nil, vigo.ErrAlreadyExists.WithArgs("Role Code in Scope") + return nil, vigo.ErrAlreadyExists.WithArgs("Role Code") } updates["code"] = newCode - updates["scope"] = newScope } } diff --git a/api/role/permissions.go b/api/role/permissions.go index 66f10bf..657cf0d 100644 --- a/api/role/permissions.go +++ b/api/role/permissions.go @@ -21,6 +21,7 @@ func getPermissions(x *vigo.X, req *GetPermissionsReq) ([]models.Permission, err type UpdatePermissionsReq struct { RoleID string `src:"path@id" desc:"Role ID"` + Scope string `json:"scope" src:"query" default:"vb" desc:"Permission Scope"` PermissionIDs []string `json:"permission_ids" src:"json" desc:"List of Permission IDs"` } @@ -35,8 +36,8 @@ func updatePermissions(x *vigo.X, req *UpdatePermissionsReq) error { } return cfg.DB().Transaction(func(tx *gorm.DB) error { - // Delete existing permissions - if err := tx.Where("role_id = ?", req.RoleID).Delete(&models.Permission{}).Error; err != nil { + // Delete existing permissions for this role AND scope + if err := tx.Where("role_id = ? AND scope = ?", req.RoleID, req.Scope).Delete(&models.Permission{}).Error; err != nil { return err } @@ -45,7 +46,7 @@ func updatePermissions(x *vigo.X, req *UpdatePermissionsReq) error { permissions := make([]models.Permission, 0, len(req.PermissionIDs)) for _, pid := range req.PermissionIDs { permissions = append(permissions, models.Permission{ - Scope: role.Scope, + Scope: req.Scope, RoleID: &req.RoleID, PermissionID: pid, Level: 7, // Default to Admin level to ensure it passes checks diff --git a/auth/auth.go b/auth/auth.go index 990f5fd..77e54c0 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -228,7 +228,7 @@ func (a *appAuth) Revoke(ctx context.Context, userID, permissionID string) error // GrantRole 授予角色 func (a *appAuth) GrantRole(ctx context.Context, userID, roleCode string) error { var role models.Role - if err := cfg.DB().Where("code = ? AND scope = ?", roleCode, a.scope).First(&role).Error; err != nil { + if err := cfg.DB().Where("code = ?", roleCode).First(&role).Error; err != nil { return err } @@ -250,7 +250,7 @@ func (a *appAuth) GrantRole(ctx context.Context, userID, roleCode string) error // RevokeRole 撤销角色 func (a *appAuth) RevokeRole(ctx context.Context, userID, roleCode string) error { var role models.Role - if err := cfg.DB().Where("code = ? AND scope = ?", roleCode, a.scope).First(&role).Error; err != nil { + if err := cfg.DB().Where("code = ?", roleCode).First(&role).Error; err != nil { return err } @@ -380,11 +380,10 @@ func (a *appAuth) init() error { for code, def := range a.roleDefs { // 1. 确保角色存在 var role models.Role - err := db.Where("code = ? AND scope = ?", code, a.scope).First(&role).Error + err := db.Where("code = ?", code).First(&role).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { role = models.Role{ - Scope: a.scope, Code: code, Name: def.name, IsSystem: true, @@ -398,13 +397,19 @@ func (a *appAuth) init() error { } } - // 2. 同步角色权限 - // 简单起见,先清除旧的,再插入新的(生产环境可能需要更精细的 diff) - // 但 Permission 表是 mixed 的,不能随便删。 - // 这里我们需要根据 RoleID 删除该角色的所有权限 - if err := db.Where("role_id = ?", role.ID).Delete(&models.Permission{}).Error; err != nil { + // 2. 同步角色权限 (Diff Sync) + // ID格式: scope:roleCode:permissionID:level + var targetIDs []string + + // 获取该角色当前scope下的所有权限ID,用于快速比对 + var existingIDs []string + if err := db.Model(&models.Permission{}).Where("role_id = ? AND scope = ?", role.ID, a.scope).Pluck("id", &existingIDs).Error; err != nil { return err } + existingMap := make(map[string]bool) + for _, id := range existingIDs { + existingMap[id] = true + } for _, policy := range def.policies { // policy 格式: "permissionID:level" @@ -412,19 +417,41 @@ func (a *appAuth) init() error { if len(parts) < 2 { continue } - // 最后一个部分是 level,前面是 permissionID levelStr := parts[len(parts)-1] permID := strings.Join(parts[:len(parts)-1], ":") var level int fmt.Sscanf(levelStr, "%d", &level) - perm := models.Permission{ - Scope: a.scope, - RoleID: &role.ID, - PermissionID: permID, - Level: level, + // 生成确定性 ID + id := fmt.Sprintf("%s:%s:%s:%d", a.scope, role.Code, permID, level) + targetIDs = append(targetIDs, id) + + // 检查是否存在 + if !existingMap[id] { + // 不存在,创建新权限 + newPerm := models.Permission{ + Scope: a.scope, + RoleID: &role.ID, + PermissionID: permID, + Level: level, + } + newPerm.ID = id + if err := db.Create(&newPerm).Error; err != nil { + return err + } + } + } + + // 3. 清理不再需要的权限 + if len(targetIDs) > 0 { + if err := db.Unscoped().Where("role_id = ? AND scope = ? AND id NOT IN ?", role.ID, a.scope, targetIDs). + Delete(&models.Permission{}).Error; err != nil { + return err } - if err := db.Create(&perm).Error; err != nil { + } else { + // 如果没有策略,删除所有 + if err := db.Unscoped().Where("role_id = ? AND scope = ?", role.ID, a.scope). + Delete(&models.Permission{}).Error; err != nil { return err } } @@ -446,19 +473,19 @@ func (a *appAuth) getUserPermissions(userID string) ([]models.Permission, error) // 2. 角色权限 // 查用户角色 - // UserRole 关联的是 RoleID,Role 表有 Scope - // 我们需要关联查询: UserRole -> Role (where scope=a.scope) + // UserRole 关联的是 RoleID + // Role 表已经没有 Scope,所以这里查出用户拥有的所有角色ID var roleIDs []string if err := db.Table("user_roles"). - Joins("JOIN roles ON roles.id = user_roles.role_id"). - Where("user_roles.user_id = ? AND roles.scope = ?", userID, a.scope). - Pluck("user_roles.role_id", &roleIDs).Error; err != nil { + Where("user_id = ?", userID). + Pluck("role_id", &roleIDs).Error; err != nil { return nil, err } if len(roleIDs) > 0 { var rolePerms []models.Permission - if err := db.Where("role_id IN ?", roleIDs).Find(&rolePerms).Error; err != nil { + // 查询这些角色在当前 scope 下拥有的权限 + if err := db.Where("role_id IN ? AND scope = ?", roleIDs, a.scope).Find(&rolePerms).Error; err != nil { return nil, err } perms = append(perms, rolePerms...) diff --git a/ui/env.js b/ui/env.js index 820d544..5c73cd2 100644 --- a/ui/env.js +++ b/ui/env.js @@ -48,13 +48,12 @@ export default async ($env) => { } } - // Role Check - if (roles && roles.length > 0) { - const hasRole = roles.some(role => vbase.hasRole(role)); - console.log(roles, hasRole, vbase.user) - if (!hasRole) { - // $env.$router.push('/403'); - // return false; + // Permission Check + if (to.meta.perm) { + if (!vbase.PermAdmin(to.meta.perm)) { + console.warn('Access denied: requires permission', to.meta.perm); + $env.$router.push('/403'); + return false; } } } diff --git a/ui/layout/ico.html b/ui/layout/ico.html index b9e16c4..ff93edb 100644 --- a/ui/layout/ico.html +++ b/ui/layout/ico.html @@ -11,7 +11,7 @@ display: flex; align-items: center; gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-xl); + padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius-xl); transition: all var(--transition-base); cursor: pointer; @@ -123,7 +123,7 @@ -
+
用户头像 {{ user.nickname || user.username }} @@ -173,7 +173,6 @@