refactor: 重构认证模型和数据库结构

v3
veypi 1 week ago
parent 8b2a1aba3b
commit 178fa755d4

@ -139,19 +139,19 @@ func refresh(x *vigo.X, req *RefreshRequest) (*AuthResponse, error) {
claims, err := jwt.ParseToken(req.RefreshToken) claims, err := jwt.ParseToken(req.RefreshToken)
if err != nil { if err != nil {
if err == jwt.ErrExpiredToken { if err == jwt.ErrExpiredToken {
return nil, vigo.ErrUnauthorized.WithString("refresh token expired") return nil, vigo.ErrTokenExpired
} }
return nil, vigo.ErrUnauthorized.WithString("invalid refresh token") return nil, vigo.ErrTokenInvalid
} }
if !jwt.IsRefreshToken(claims) { if !jwt.IsRefreshToken(claims) {
return nil, vigo.ErrUnauthorized.WithString("invalid token type") return nil, vigo.ErrTokenInvalid
} }
// 查找用户 // 查找用户
var user models.User var user models.User
if err := cfg.DB().First(&user, "id = ?", claims.UserID).Error; err != nil { if err := cfg.DB().First(&user, "id = ?", claims.UserID).Error; err != nil {
return nil, vigo.ErrUnauthorized.WithString("user not found") return nil, vigo.ErrTokenInvalid
} }
if user.Status != models.UserStatusActive { if user.Status != models.UserStatusActive {

@ -91,11 +91,11 @@ func handleRefreshToken(req *TokenRequest) (*TokenResponse, error) {
// 查找刷新令牌 // 查找刷新令牌
var token models.OAuthToken var token models.OAuthToken
if err := cfg.DB().First(&token, "refresh_token = ?", req.RefreshToken).Error; err != nil { if err := cfg.DB().First(&token, "refresh_token = ?", req.RefreshToken).Error; err != nil {
return nil, vigo.ErrUnauthorized.WithString("invalid refresh token") return nil, vigo.ErrTokenInvalid
} }
if token.Revoked { if token.Revoked {
return nil, vigo.ErrUnauthorized.WithString("token has been revoked") return nil, vigo.ErrTokenInvalid
} }
// 生成新的访问令牌 // 生成新的访问令牌

@ -111,9 +111,7 @@ func (f *authFactory) New(appKey string, config models.AppConfig) Auth {
return auth return auth
} }
var ( var validResourceRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`)
validResourceRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`)
)
func validatePermissionID(permissionID string) { func validatePermissionID(permissionID string) {
if permissionID == "*:*" { if permissionID == "*:*" {
@ -598,8 +596,6 @@ func (a *appAuth) CheckPermission(ctx context.Context, userID, orgID, permission
} }
func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error) { func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error) {
fmt.Printf("[DEBUG] CheckPermission: userID=%s, orgID=%s, permID=%s, resID=%s\n", userID, orgID, permissionID, resourceID)
// 1. 检查用户是否有该权限的角色(包括当前组织角色和系统全局角色) // 1. 检查用户是否有该权限的角色(包括当前组织角色和系统全局角色)
var roleIDs []string var roleIDs []string
roleQuery := cfg.DB().Model(&models.UserRole{}). roleQuery := cfg.DB().Model(&models.UserRole{}).
@ -612,10 +608,8 @@ func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissi
} }
if err := roleQuery.Pluck("role_id", &roleIDs).Error; err != nil { if err := roleQuery.Pluck("role_id", &roleIDs).Error; err != nil {
fmt.Printf("[DEBUG] CheckPermission: failed to get roles: %v\n", err)
return false, err return false, err
} }
fmt.Printf("[DEBUG] CheckPermission: roleIDs=%v\n", roleIDs)
if len(roleIDs) > 0 { if len(roleIDs) > 0 {
// 构造可能的通配符权限ID // 构造可能的通配符权限ID

@ -29,9 +29,9 @@ func AuthMiddleware() func(*vigo.X) error {
claims, err := jwt.ParseToken(tokenString) claims, err := jwt.ParseToken(tokenString)
if err != nil { if err != nil {
if err == jwt.ErrExpiredToken { if err == jwt.ErrExpiredToken {
return vigo.ErrUnauthorized.WithString("token expired") return vigo.ErrTokenExpired
} }
return vigo.ErrUnauthorized.WithString("invalid token") return vigo.ErrTokenInvalid
} }
// 检查token是否在黑名单中 // 检查token是否在黑名单中

@ -9,7 +9,7 @@ package models
import ( import (
"time" "time"
"gorm.io/gorm" "github.com/veypi/vigo"
) )
// 角色代码常量 // 角色代码常量
@ -22,10 +22,9 @@ const (
// Permission 权限定义表(权限字典) // Permission 权限定义表(权限字典)
// ID 格式: app:resource:action (例如: crm:customer:read) // ID 格式: app:resource:action (例如: crm:customer:read)
type Permission struct { type Permission struct {
ID string `json:"id" gorm:"primaryKey;size:100" desc:"权限ID格式: app:resource:action"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
ID string `json:"id" gorm:"primaryKey;size:100" desc:"权限ID格式: app:resource:action"`
AppKey string `json:"app_key" gorm:"index;size:50" desc:"应用标识"` AppKey string `json:"app_key" gorm:"index;size:50" desc:"应用标识"`
Resource string `json:"resource" gorm:"index;size:50" desc:"资源类型"` Resource string `json:"resource" gorm:"index;size:50" desc:"资源类型"`
Action string `json:"action" gorm:"index;size:50" desc:"操作类型"` Action string `json:"action" gorm:"index;size:50" desc:"操作类型"`
@ -38,13 +37,16 @@ func (Permission) TableName() string {
// Role 角色表(不关联 app可跨应用 // Role 角色表(不关联 app可跨应用
type Role struct { type Role struct {
Base vigo.Model
OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID空=系统预设"` OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID空=系统预设"`
Code string `json:"code" gorm:"index;size:50" desc:"角色代码"` Code string `json:"code" gorm:"index;size:50" desc:"角色代码"`
Name string `json:"name" desc:"角色名称"` Name string `json:"name" desc:"角色名称"`
Description string `json:"description" desc:"角色描述"` Description string `json:"description" desc:"角色描述"`
IsSystem bool `json:"is_system" desc:"是否系统预设角色"` IsSystem bool `json:"is_system" desc:"是否系统预设角色"`
Status int `json:"status" gorm:"default:1" desc:"状态: 1=启用, 0=禁用"` Status int `json:"status" gorm:"default:1" desc:"状态: 1=启用, 0=禁用"`
// 外键关联
Org *Org `json:"org,omitempty" gorm:"foreignKey:OrgID;references:ID"`
} }
func (Role) TableName() string { func (Role) TableName() string {
@ -53,10 +55,14 @@ func (Role) TableName() string {
// RolePermission 角色权限关联表 // RolePermission 角色权限关联表
type RolePermission struct { type RolePermission struct {
Base vigo.Model
RoleID string `json:"role_id" gorm:"index;size:36" desc:"角色ID"` RoleID string `json:"role_id" gorm:"index;size:36" desc:"角色ID"`
PermissionID string `json:"permission_id" gorm:"index;size:100" desc:"权限ID"` PermissionID string `json:"permission_id" gorm:"index;size:100" desc:"权限ID"`
Condition string `json:"condition" gorm:"size:20;default:'none'" desc:"权限条件: none/owner/admin"` Condition string `json:"condition" gorm:"size:20;default:'none'" desc:"权限条件: none/owner/admin"`
// 外键关联
Role Role `json:"role,omitempty" gorm:"foreignKey:RoleID;references:ID"`
Permission Permission `json:"permission,omitempty" gorm:"foreignKey:PermissionID;references:ID"`
} }
func (RolePermission) TableName() string { func (RolePermission) TableName() string {
@ -65,11 +71,16 @@ func (RolePermission) TableName() string {
// UserRole 用户角色关联表 // UserRole 用户角色关联表
type UserRole struct { type UserRole struct {
Base vigo.Model
UserID string `json:"user_id" gorm:"index;size:36" desc:"用户ID"` UserID string `json:"user_id" gorm:"index;size:36" desc:"用户ID"`
OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID"` OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID"`
RoleID string `json:"role_id" gorm:"index;size:36" desc:"角色ID"` RoleID string `json:"role_id" gorm:"index;size:36" desc:"角色ID"`
ExpireAt *time.Time `json:"expire_at" desc:"过期时间(可选)"` ExpireAt *time.Time `json:"expire_at" desc:"过期时间(可选)"`
// 外键关联
User User `json:"user,omitempty" gorm:"foreignKey:UserID;references:ID"`
Org *Org `json:"org,omitempty" gorm:"foreignKey:OrgID;references:ID"`
Role Role `json:"role,omitempty" gorm:"foreignKey:RoleID;references:ID"`
} }
func (UserRole) TableName() string { func (UserRole) TableName() string {
@ -78,13 +89,18 @@ func (UserRole) TableName() string {
// UserPermission 用户特定资源权限表(数据级权限) // UserPermission 用户特定资源权限表(数据级权限)
type UserPermission struct { type UserPermission struct {
Base vigo.Model
UserID string `json:"user_id" gorm:"index;size:36" desc:"用户ID"` UserID string `json:"user_id" gorm:"index;size:36" desc:"用户ID"`
OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID"` OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID"`
PermissionID string `json:"permission_id" gorm:"index;size:100" desc:"权限ID"` PermissionID string `json:"permission_id" gorm:"index;size:100" desc:"权限ID"`
ResourceID string `json:"resource_id" gorm:"index;size:100" desc:"具体资源ID* 表示所有"` ResourceID string `json:"resource_id" gorm:"index;size:100" desc:"具体资源ID* 表示所有"`
ExpireAt *time.Time `json:"expire_at" desc:"过期时间(可选)"` ExpireAt *time.Time `json:"expire_at" desc:"过期时间(可选)"`
GrantedBy string `json:"granted_by" gorm:"size:36" desc:"授权人ID"` GrantedBy string `json:"granted_by" gorm:"size:36" desc:"授权人ID"`
// 外键关联
User User `json:"user,omitempty" gorm:"foreignKey:UserID;references:ID"`
Org *Org `json:"org,omitempty" gorm:"foreignKey:OrgID;references:ID"`
Permission Permission `json:"permission,omitempty" gorm:"foreignKey:PermissionID;references:ID"`
} }
func (UserPermission) TableName() string { func (UserPermission) TableName() string {

@ -1,30 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// Base 基础模型
type Base struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// BeforeCreate 自动生成UUID
func (b *Base) BeforeCreate(tx *gorm.DB) error {
if b.ID == "" {
b.ID = uuid.New().String()
}
return nil
}

@ -8,11 +8,13 @@ package models
import ( import (
"time" "time"
"github.com/veypi/vigo"
) )
// OAuthClient OAuth2.0 客户端 // OAuthClient OAuth2.0 客户端
type OAuthClient struct { type OAuthClient struct {
Base vigo.Model
ClientID string `json:"client_id" gorm:"uniqueIndex;size:100;not null"` ClientID string `json:"client_id" gorm:"uniqueIndex;size:100;not null"`
ClientSecret string `json:"-" gorm:"size:255;not null"` ClientSecret string `json:"-" gorm:"size:255;not null"`
Name string `json:"name" gorm:"size:100;not null"` Name string `json:"name" gorm:"size:100;not null"`
@ -22,6 +24,10 @@ type OAuthClient struct {
OwnerID string `json:"owner_id" gorm:"not null"` OwnerID string `json:"owner_id" gorm:"not null"`
OrgID string `json:"org_id" gorm:"index"` OrgID string `json:"org_id" gorm:"index"`
Status int `json:"status" gorm:"default:1"` Status int `json:"status" gorm:"default:1"`
// 外键关联
Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID;references:ID"`
Org *Org `json:"org,omitempty" gorm:"foreignKey:OrgID;references:ID"`
} }
func (OAuthClient) TableName() string { func (OAuthClient) TableName() string {
@ -30,7 +36,7 @@ func (OAuthClient) TableName() string {
// OAuthAuthorizationCode OAuth2.0 授权码 // OAuthAuthorizationCode OAuth2.0 授权码
type OAuthAuthorizationCode struct { type OAuthAuthorizationCode struct {
Base vigo.Model
Code string `json:"code" gorm:"uniqueIndex;size:100;not null"` Code string `json:"code" gorm:"uniqueIndex;size:100;not null"`
ClientID string `json:"client_id" gorm:"index;not null"` ClientID string `json:"client_id" gorm:"index;not null"`
UserID string `json:"user_id" gorm:"index;not null"` UserID string `json:"user_id" gorm:"index;not null"`
@ -41,6 +47,11 @@ type OAuthAuthorizationCode struct {
CodeChallengeMethod string `json:"-" gorm:"size:10"` CodeChallengeMethod string `json:"-" gorm:"size:10"`
ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
Used bool `json:"used" gorm:"default:false"` Used bool `json:"used" gorm:"default:false"`
// 外键关联
Client OAuthClient `json:"client,omitempty" gorm:"foreignKey:ClientID;references:ID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID;references:ID"`
Org *Org `json:"org,omitempty" gorm:"foreignKey:OrgID;references:ID"`
} }
func (OAuthAuthorizationCode) TableName() string { func (OAuthAuthorizationCode) TableName() string {
@ -49,7 +60,7 @@ func (OAuthAuthorizationCode) TableName() string {
// OAuthToken OAuth2.0 令牌 // OAuthToken OAuth2.0 令牌
type OAuthToken struct { type OAuthToken struct {
Base vigo.Model
ClientID string `json:"client_id" gorm:"index;not null"` ClientID string `json:"client_id" gorm:"index;not null"`
UserID string `json:"user_id" gorm:"index;not null"` UserID string `json:"user_id" gorm:"index;not null"`
OrgID string `json:"org_id" gorm:"index"` OrgID string `json:"org_id" gorm:"index"`
@ -59,6 +70,11 @@ type OAuthToken struct {
Scope string `json:"scope" gorm:"size:200"` Scope string `json:"scope" gorm:"size:200"`
ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked" gorm:"default:false"` Revoked bool `json:"revoked" gorm:"default:false"`
// 外键关联
Client OAuthClient `json:"client,omitempty" gorm:"foreignKey:ClientID;references:ID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID;references:ID"`
Org *Org `json:"org,omitempty" gorm:"foreignKey:OrgID;references:ID"`
} }
func (OAuthToken) TableName() string { func (OAuthToken) TableName() string {

@ -6,9 +6,13 @@
package models package models
import (
"github.com/veypi/vigo"
)
// Org 组织/租户 // Org 组织/租户
type Org struct { type Org struct {
Base vigo.Model
Name string `json:"name" gorm:"size:50;not null"` Name string `json:"name" gorm:"size:50;not null"`
Code string `json:"code" gorm:"uniqueIndex;size:30;not null"` Code string `json:"code" gorm:"uniqueIndex;size:30;not null"`
OwnerID string `json:"owner_id" gorm:"not null"` OwnerID string `json:"owner_id" gorm:"not null"`
@ -21,6 +25,11 @@ type Org struct {
Settings string `json:"-" gorm:"type:text"` // JSON配置 Settings string `json:"-" gorm:"type:text"` // JSON配置
Status int `json:"status" gorm:"default:1"` Status int `json:"status" gorm:"default:1"`
MaxMembers int `json:"max_members" gorm:"default:100"` MaxMembers int `json:"max_members" gorm:"default:100"`
// 外键关联
Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID;references:ID"`
Parent *Org `json:"parent,omitempty" gorm:"foreignKey:ParentID;references:ID"`
Leader *User `json:"leader,omitempty" gorm:"foreignKey:LeaderID;references:ID"`
} }
func (Org) TableName() string { func (Org) TableName() string {
@ -29,7 +38,7 @@ func (Org) TableName() string {
// OrgMember 组织成员关系 // OrgMember 组织成员关系
type OrgMember struct { type OrgMember struct {
Base vigo.Model
OrgID string `json:"org_id" gorm:"type:varchar(36);uniqueIndex:idx_org_user;not null"` OrgID string `json:"org_id" gorm:"type:varchar(36);uniqueIndex:idx_org_user;not null"`
UserID string `json:"user_id" gorm:"type:varchar(36);uniqueIndex:idx_org_user;not null"` UserID string `json:"user_id" gorm:"type:varchar(36);uniqueIndex:idx_org_user;not null"`
RoleIDs string `json:"role_ids" gorm:"size:200"` // 逗号分隔 RoleIDs string `json:"role_ids" gorm:"size:200"` // 逗号分隔
@ -37,6 +46,10 @@ type OrgMember struct {
Department string `json:"department" gorm:"size:50"` Department string `json:"department" gorm:"size:50"`
JoinedAt string `json:"joined_at"` JoinedAt string `json:"joined_at"`
Status int `json:"status" gorm:"default:1"` // 0:待审核 1:正常 2:禁用 Status int `json:"status" gorm:"default:1"` // 0:待审核 1:正常 2:禁用
// 外键关联
Org Org `json:"org,omitempty" gorm:"foreignKey:OrgID;references:ID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID;references:ID"`
} }
func (OrgMember) TableName() string { func (OrgMember) TableName() string {

@ -8,11 +8,13 @@ package models
import ( import (
"time" "time"
"github.com/veypi/vigo"
) )
// User 全局用户表 // User 全局用户表
type User struct { type User struct {
Base vigo.Model
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"` Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
Password string `json:"-" gorm:"size:255"` // bcrypt hash Password string `json:"-" gorm:"size:255"` // bcrypt hash
Nickname string `json:"nickname" gorm:"size:50"` Nickname string `json:"nickname" gorm:"size:50"`
@ -32,7 +34,7 @@ func (User) TableName() string {
// Identity 第三方身份绑定 // Identity 第三方身份绑定
type Identity struct { type Identity struct {
Base vigo.Model
UserID string `json:"user_id" gorm:"index;not null"` UserID string `json:"user_id" gorm:"index;not null"`
Provider string `json:"provider" gorm:"size:20;not null"` // google/github/wechat/ldap Provider string `json:"provider" gorm:"size:20;not null"` // google/github/wechat/ldap
ProviderUID string `json:"provider_uid" gorm:"index;size:100;not null"` // 第三方唯一ID ProviderUID string `json:"provider_uid" gorm:"index;size:100;not null"` // 第三方唯一ID
@ -42,6 +44,9 @@ type Identity struct {
AccessToken string `json:"-" gorm:"size:500"` AccessToken string `json:"-" gorm:"size:500"`
RefreshToken string `json:"-" gorm:"size:500"` RefreshToken string `json:"-" gorm:"size:500"`
ExpiresAt *time.Time `json:"-"` ExpiresAt *time.Time `json:"-"`
// 外键关联
User User `json:"user,omitempty" gorm:"foreignKey:UserID;references:ID"`
} }
func (Identity) TableName() string { func (Identity) TableName() string {
@ -50,7 +55,7 @@ func (Identity) TableName() string {
// Session 登录会话 // Session 登录会话
type Session struct { type Session struct {
Base vigo.Model
UserID string `json:"user_id" gorm:"index;not null"` UserID string `json:"user_id" gorm:"index;not null"`
TokenID string `json:"token_id" gorm:"uniqueIndex;size:36"` // JWT jti TokenID string `json:"token_id" gorm:"uniqueIndex;size:36"` // JWT jti
Type string `json:"type" gorm:"size:20;not null"` // access/refresh Type string `json:"type" gorm:"size:20;not null"` // access/refresh
@ -59,6 +64,9 @@ type Session struct {
ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked" gorm:"default:false"` Revoked bool `json:"revoked" gorm:"default:false"`
RevokedAt *time.Time `json:"revoked_at"` RevokedAt *time.Time `json:"revoked_at"`
// 外键关联
User User `json:"user,omitempty" gorm:"foreignKey:UserID;references:ID"`
} }
func (Session) TableName() string { func (Session) TableName() string {

@ -118,7 +118,7 @@
<i class="fas" :class="collapsed ? 'fa-indent' : 'fa-outdent'"></i> <i class="fas" :class="collapsed ? 'fa-indent' : 'fa-outdent'"></i>
</div> </div>
<div class="breadcrumb"> <div class="breadcrumb">
{{ currentRouteName }} {{ getRouteName() }}
</div> </div>
</div> </div>
@ -165,7 +165,11 @@
{label: () => $t('nav.oauth'), icon: "<i class='fas fa-key'></i>", path: "/oauth/apps"}, {label: () => $t('nav.oauth'), icon: "<i class='fas fa-key'></i>", path: "/oauth/apps"},
]; ];
currentRouteName = ""; getRouteName = () => {
const path = $router.current.path;
const item = menuItems.find(i => i.path === path);
return item ? item.label() : path;
};
toggleCollapse = () => { toggleCollapse = () => {
collapsed = !collapsed; collapsed = !collapsed;
@ -185,13 +189,5 @@
$router.push('/org'); $router.push('/org');
}; };
</script> </script>
<script>
$router.onChange(() => {
const path = $router.current.path;
console.log(path)
const item = $data.menuItems.find(i => i.path === path);
$data.currentRouteName = item ? item.label : path;
});
</script>
</html> </html>

Loading…
Cancel
Save