@ -1,6 +1,9 @@
/ * *
* VBase - Scoped RBAC 权限管理客户端
* 对应后端 : github . com / veypi / vigo / contrib / auth . Auth 接口
*
* Token 基于 HttpOnly Cookie 管理 , JS 不可读 , 防沙箱泄露 。
* 非浏览器客户端仍可通过 Authorization header 传递 token 。
* /
// 权限等级常量 (与后端一致)
@ -21,60 +24,53 @@ class VBase {
this . baseURL = baseURL ;
this . scope = scope ;
this . login _page = login _page || ( baseURL + '/login' )
this . tokenKey = ` token ` ;
this . refreshTokenKey = ` refresh_token ` ;
this . userKey = ` vbase_user ` ;
this . login _page = login _page || ( baseURL + '/login' ) ;
this . userKey = 'vbase_user' ;
this . users = users ;
this . _token = localStorage . getItem ( this . tokenKey ) || '' ;
this . _refreshToken = localStorage . getItem ( this . refreshTokenKey ) || '' ;
this . _user = JSON . parse ( localStorage . getItem ( this . userKey ) || 'null' ) ;
this . _user = null ;
try {
const cached = JSON . parse ( localStorage . getItem ( this . userKey ) ) ;
if ( cached ) {
this . _user = cached ;
this . _cachePublicUser ( cached ) ;
}
} catch ( e ) { }
this . _pendingUserIDs = new Set ( ) ;
this . _loadingUserIDs = new Set ( ) ;
this . _resolvedUserIDs = new Set ( ) ;
this . _pendingUserFlush = null ;
this . _cachePublicUser ( this . _user ) ;
if ( this . _token ) {
this . fetchUser ( )
}
}
// ========== Getters ==========
get token ( ) { return this . _token ; }
get refreshToken ( ) { return this . _refreshToken ; }
get user ( ) { return this . _user ; }
// ========== Setters ==========
set token ( val ) {
this . _token = val ;
if ( val ) localStorage . setItem ( this . tokenKey , val ) ;
else localStorage . removeItem ( this . tokenKey ) ;
// 验证登录状态
this . fetchUser ( ) . catch ( ( ) => { } ) ;
}
set refreshToken ( val ) {
this . _refreshToken = val ;
if ( val ) localStorage . setItem ( this . refreshTokenKey , val ) ;
else localStorage . removeItem ( this . refreshTokenKey ) ;
}
// ========== Getters / Setters ==========
get user ( ) { return this . _user ; }
set user ( val ) {
this . _user = val ;
if ( val ) localStorage . setItem ( this . userKey , JSON . stringify ( val ) ) ;
else localStorage . removeItem ( this . userKey ) ;
if ( val ) {
localStorage . setItem ( this . userKey , JSON . stringify ( val ) ) ;
} else {
localStorage . removeItem ( this . userKey ) ;
}
this . _cachePublicUser ( val ) ;
}
// ========== API 请求 ==========
async request ( method , path , data = null , headers = { } ) {
const url = ` ${ this . baseURL } ${ path } ` ;
const config = {
method ,
headers : {
'Content-Type' : 'application/json' ,
... this . getAuthHeaders ( ) ,
... headers
}
... headers ,
} ,
} ;
if ( data ) config . body = JSON . stringify ( data ) ;
@ -88,93 +84,66 @@ class VBase {
}
if ( resData . code && resData . code !== 200 ) {
throw new Error ( resData . message || 'API Error' ) ;
const error = new Error ( resData . message || 'API Error' ) ;
Object . assign ( error , resData ) ;
throw error ;
}
return resData . data || resData ;
}
// ========== 认证相关 ==========
// ========== 认证 ==========
/** 用户名密码登录 */
async login ( username , password ) {
const data = await this . request ( 'POST' , '/api/auth/login' , { username , password } ) ;
if ( data . access _token ) {
this . token = data . access _token ;
if ( data . refresh _token ) this . refreshToken = data . refresh _token ;
this . user = data . user
this . fetchUser ( )
if ( data . user ) {
this . user = data . user ;
await this . fetchUser ( ) ;
return true ;
}
return false ;
}
/ * *
* OAuth Callback Handler
* @ param { string } provider
* @ param { string } code
* @ param { string } state
* @ returns { Promise < Object > } Response data
* /
/** OAuth 回调 */
async oauthCallback ( provider , code , state ) {
const data = await this . request ( 'GET' , ` /api/auth/callback/ ${ provider } ?code= ${ code } &state= ${ state } ` ) ;
// If login success directly
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 ( )
if ( data . user ) {
this . user = data . user ;
await this . fetchUser ( ) ;
}
return data ;
}
/ * *
* Bind existing account
* @ param { string } tempToken
* @ param { string } username
* @ param { string } password
* @ returns { Promise < Object > }
* /
/** 绑定已有账号 */
async bindAccount ( tempToken , username , password ) {
const data = await this . request ( 'POST' , '/api/auth/bind' , {
temp _token : tempToken ,
username ,
password
password ,
} ) ;
if ( data . access _token || data . token ) {
this . token = data . access _token || data . token ;
if ( data . refresh _token ) this . refreshToken = data . refresh _token ;
this . fetchUser ( )
if ( data . user ) {
this . user = data . user ;
await this . fetchUser ( ) ;
}
return data ;
}
/ * *
* Register and bind new account
* @ param { string } tempToken
* @ param { string } username
* @ param { string } email
* @ returns { Promise < Object > }
* /
/** 绑定并注册 */
async bindRegister ( tempToken , username , email ) {
const data = await this . request ( 'POST' , '/api/auth/bind-register' , {
temp _token : tempToken ,
username ,
email
email ,
} ) ;
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 ( )
if ( data . user ) {
this . user = data . user ;
await this . fetchUser ( ) ;
}
return data ;
}
/** 登出 */
async logout ( redirect ) {
try {
await this . request ( 'POST' , '/api/auth/logout' ) ;
@ -187,39 +156,30 @@ class VBase {
}
}
/** Token 刷新(后端自动处理,前端可主动调用) */
async refresh ( ) {
if ( ! this . refreshToken ) return false ;
try {
const data = await this . request ( 'POST' , '/api/auth/refresh' , { refresh _token : this . refreshToken } ) ;
if ( data . access _token ) {
this . token = data . access _token ;
if ( data . refresh _token ) this . refreshToken = data . refresh _token ;
await this . request ( 'POST' , '/api/auth/refresh' , { } ) ;
return true ;
}
return false ;
} catch ( e ) {
console . warn ( e )
return false
return false ;
}
}
/** 获取当前用户信息及权限 */
async fetchUser ( ) {
const user = await this . request ( 'GET' , '/api/auth/me' ) . catch ( e => {
this . clear ( )
} ) ;
try {
const user = await this . request ( 'GET' , '/api/auth/me' ) ;
this . user = user ;
return user ;
} catch ( e ) {
this . clear ( ) ;
throw e ;
}
getAuthHeaders ( ) {
const headers = { } ;
if ( this . token ) headers [ 'Authorization' ] = ` Bearer ${ this . token } ` ;
return headers ;
}
/** 清除登录状态 */
clear ( ) {
this . token = '' ;
this . refreshToken = '' ;
this . user = null ;
for ( const id of Object . keys ( this . users ) ) {
delete this . users [ id ] ;
@ -230,6 +190,8 @@ class VBase {
this . _pendingUserFlush = null ;
}
// ========== 用户 ==========
User ( id ) {
if ( ! id ) return { } ;
@ -247,28 +209,8 @@ class VBase {
return this . users [ id ] ;
}
isExpired ( token ) {
if ( ! token ) token = this . token ;
if ( ! token ) return true ;
try {
const base64Url = token . split ( '.' ) [ 1 ] ;
const base64 = base64Url . replace ( /-/g , '+' ) . replace ( /_/g , '/' ) ;
const payload = JSON . parse ( window . atob ( base64 ) ) ;
const now = Math . floor ( Date . now ( ) / 1000 ) ;
return payload . exp && payload . exp < now ;
} catch ( e ) {
return true ;
}
}
// ========== 权限检查 (与后端接口一致) ==========
// ========== 权限检查 ==========
/ * *
* 检查权限
* @ param { string } code - 权限码 , 如 "resource:instance"
* @ param { number } level - 需要的权限等级
* @ returns { boolean }
* /
Perm ( code , level = Level . Read ) {
if ( ! this . user ) return false ;
if ( ! code ) return true ;
@ -277,133 +219,33 @@ class VBase {
return this . _checkPermissionLevel ( perms , code , level ) ;
}
/ * *
* 检查创建权限 ( level 1 , 检查奇数层 )
* @ param { string } code - 权限码
* @ returns { boolean }
* /
PermCreate ( code ) {
return this . Perm ( code , Level . Create ) ;
}
/ * *
* 检查读取权限 ( level 2 , 检查偶数层 )
* @ param { string } code - 权限码
* @ returns { boolean }
* /
PermRead ( code ) {
return this . Perm ( code , Level . Read ) ;
}
/ * *
* 检查更新权限 ( level 4 , 检查偶数层 )
* @ param { string } code - 权限码
* @ returns { boolean }
* /
PermWrite ( code ) {
return this . Perm ( code , Level . Write ) ;
}
/ * *
* 检查管理员权限 ( level 7 , 检查偶数层 )
* @ param { string } code - 权限码
* @ returns { boolean }
* /
PermAdmin ( code ) {
return this . Perm ( code , Level . Admin ) ;
}
PermCreate ( code ) { return this . Perm ( code , Level . Create ) ; }
PermRead ( code ) { return this . Perm ( code , Level . Read ) ; }
PermWrite ( code ) { return this . Perm ( code , Level . Write ) ; }
PermAdmin ( code ) { return this . Perm ( code , Level . Admin ) ; }
// ========== 内部方法 ==========
/ * *
* 核心权限检查逻辑 ( 与后端 checkPermissionLevel 一致 )
* /
_checkPermissionLevel ( perms , targetPermID , requiredLevel ) {
for ( const p of perms ) {
const permID = p . permission _id || p ;
const permLevel = p . level !== undefined ? p . level : Level . Read ;
// 1. 管理员特权 (Level 7 且是父级或同级)
if ( permLevel === Level . Admin ) {
if ( permID === '*' || targetPermID . startsWith ( permID + ':' ) || permID === targetPermID ) {
return true ;
}
}
// 2. 普通权限匹配
if ( permLevel >= requiredLevel ) {
if ( permID === targetPermID ) {
return true ;
}
}
}
return false ;
}
// ========== Axios 集成 ==========
wrapAxios ( axiosInstance ) {
// 请求拦截器
axiosInstance . interceptors . request . use ( config => {
const headers = this . getAuthHeaders ( ) ;
for ( const key in headers ) {
config . headers [ key ] = headers [ key ] ;
}
return config ;
} , error => Promise . reject ( error ) ) ;
// 响应拦截器
axiosInstance . interceptors . response . use (
response => response ,
async error => {
const originalRequest = error . config ;
if ( error . response ? . status === 401 && ! originalRequest . _retry ) {
originalRequest . _retry = true ;
try {
await this . refresh ( ) ;
const headers = this . getAuthHeaders ( ) ;
originalRequest . headers [ 'Authorization' ] = headers [ 'Authorization' ] ;
return axiosInstance ( originalRequest ) ;
} catch ( e ) {
this . logout ( ) ;
return Promise . reject ( e ) ;
}
}
return Promise . reject ( error ) ;
}
) ;
}
wrapFetch ( urlprefix ) {
const originalFetch = window . fetch ;
const self = this ;
return async function wrappedFetch ( input , init = { } ) {
let url ;
if ( typeof input === 'string' ) {
url = input ;
} else if ( input instanceof Request ) {
url = input . url ;
} else {
url = String ( input ) ;
}
if ( url . startsWith ( '@' ) ) {
url = url . slice ( 1 )
} else if ( urlprefix && ! url . startsWith ( 'http://' ) && ! url . startsWith ( 'https://' ) ) {
url = urlprefix + url ;
}
const headers = { ... init . headers } ;
const authHeaders = self . getAuthHeaders ( ) ;
for ( const [ key , value ] of Object . entries ( authHeaders ) ) {
if ( ! headers [ key ] ) {
headers [ key ] = value ;
}
}
return originalFetch ( url , { ... init , headers } ) ;
} ;
}
_cachePublicUser ( user ) {
if ( ! user ? . id ) return ;