feat(auth): Improve permission system and role management

- Add Scope and Level fields to UserPermissionInfo response
    - Include role-based permissions in /auth/me endpoint
    - Implement diff-based permission sync for role initialization
    - Remove Scope field from Role model queries (create, patch, grant)
    - Add permission-based route guards in UI (perm: '*')
    - Fix register to return error on default role assignment failure
    - Fix token refresh to only fetch user when token exists
    - Fix code formatting in api/init.go (remove extra spaces)
master
veypi 3 weeks ago
parent e83444df4c
commit 92156dcd53

@ -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,
})
}

@ -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

@ -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{}
// 应用配置

@ -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

@ -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
}
}

@ -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

@ -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 关联的是 RoleIDRole 表有 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...)

@ -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;
}
}
}

@ -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 @@
</style>
<body>
<div class="header-user ml-auto" v-if="user.id" @click="toggleDropdown">
<div class="header-user" v-if="user.id" @click="toggleDropdown">
<img :src="user.avatar" class="user-avatar" alt="用户头像">
<span class="user-name">{{ user.nickname || user.username }}</span>
<span class="dropdown-arrow" :class="{open: dropdownOpen}"></span>
@ -173,7 +173,6 @@
</script>
<script>
console.log(user.avatar)
// 点击外部区域关闭下拉菜单
document.addEventListener('click', (event) => {
if (!$node.contains(event.target)) {

@ -12,10 +12,10 @@ const routes = [
},
// Role Management
{ path: '/roles', component: '/page/sys/role/index.html', layout: 'default', meta: { auth: true } },
{ path: '/roles', component: '/page/sys/role/index.html', layout: 'default', meta: { auth: true, perm: '*' } },
// Settings Management
{ path: '/settings', component: '/page/sys/settings.html', layout: 'default', meta: { auth: true } },
{ path: '/settings', component: '/page/sys/settings.html', layout: 'default', meta: { auth: true, perm: '*' } },
// User System
{ path: '/profile', component: '/page/user/profile.html', layout: 'default', meta: { auth: true } },
@ -23,12 +23,12 @@ const routes = [
path: '/users',
component: '/page/sys/user/index.html',
layout: 'default',
meta: { auth: true, roles: ['admin'] }
meta: { auth: true, perm: '*' }
},
// OAuth Management
{ path: '/oauth/apps', component: '/page/sys/oauth/index.html', layout: 'default', meta: { auth: true } },
{ path: '/oauth/providers', component: '/page/sys/oauth/providers.html', layout: 'default', meta: { auth: true } },
{ path: '/oauth/apps', component: '/page/sys/oauth/index.html', layout: 'default', meta: { auth: true, perm: '*' } },
{ path: '/oauth/providers', component: '/page/sys/oauth/providers.html', layout: 'default', meta: { auth: true, perm: '*' } },
// Errors
{ path: '/403', component: '/page/403.html' },

@ -29,6 +29,9 @@ class VBase {
this._token = localStorage.getItem(this.tokenKey) || '';
this._refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null');
if (this._token) {
this.fetchUser()
}
}
// ========== Getters ==========
@ -90,7 +93,8 @@ class VBase {
if (data.access_token) {
this.token = data.access_token;
if (data.refresh_token) this.refreshToken = data.refresh_token;
this.user = data.user;
this.user = data.user
this.fetchUser()
return true;
}
return false;
@ -111,6 +115,7 @@ class VBase {
this.token = data.access_token || data.token;
if (data.refresh_token) this.refreshToken = data.refresh_token;
if (data.user) this.user = data.user;
this.fetchUser()
}
return data;
@ -133,7 +138,7 @@ class VBase {
if (data.access_token || data.token) {
this.token = data.access_token || data.token;
if (data.refresh_token) this.refreshToken = data.refresh_token;
if (data.user) this.user = data.user;
this.fetchUser()
}
return data;
@ -157,6 +162,7 @@ class VBase {
this.token = data.access_token || data.token;
if (data.refresh_token) this.refreshToken = data.refresh_token;
if (data.user) this.user = data.user;
this.fetchUser()
}
return data;

Loading…
Cancel
Save