diff --git a/ui/env.js b/ui/env.js index 073ab60..0a53ae3 100644 --- a/ui/env.js +++ b/ui/env.js @@ -12,7 +12,6 @@ export default async ($env) => { // Initialize VBase Service const vbase = new VBase('vb', '/'); // Relative path - console.log(vbase) $env.$vbase = vbase; // Wrap Axios @@ -21,7 +20,6 @@ export default async ($env) => { // Router Guard $env.$router.beforeEnter = async (to, from, next) => { const isAuth = to.meta && to.meta.auth; - const isGuest = to.meta && to.meta.guest; const roles = to.meta && to.meta.roles; // Array of required roles if (isAuth) { diff --git a/ui/vbase.js b/ui/vbase.js index 12b4eb6..295fb03 100644 --- a/ui/vbase.js +++ b/ui/vbase.js @@ -1,32 +1,41 @@ +/** + * VBase - Scoped RBAC 权限管理客户端 + * 对应后端: github.com/veypi/vigo/contrib/auth.Auth 接口 + */ + +// 权限等级常量 (与后端一致) +export const Level = { + None: 0, + Create: 1, // 001 创建 (奇数层) + Read: 2, // 010 读取 (偶数层) + Write: 4, // 100 写入 (偶数层) + ReadWrite: 6, // 110 读写 (偶数层) + Admin: 7, // 111 管理员 (完全控制) +}; class VBase { constructor(scope, baseURL) { if (!scope) throw new Error('VBase: scope is required'); - if (!baseURL) throw new Error('VBase: baseURL is required'); - if (!baseURL) { - baseURL = '' - } - if (baseURL === '' || baseURL === '/') { - baseURL = window.location.origin - } + if (!baseURL) baseURL = window.location.origin; + if (baseURL === '' || baseURL === '/') baseURL = window.location.origin; this.baseURL = baseURL; this.scope = scope; - this.tokenKey = 'vbase_access_token'; - this.refreshTokenKey = 'vbase_refresh_token'; - this.userKey = 'vbase_user_info'; + this.tokenKey = `vbase_token`; + this.refreshTokenKey = `vbase_refresh_token`; + this.userKey = `vbase_user`; this._token = localStorage.getItem(this.tokenKey) || ''; this._refreshToken = localStorage.getItem(this.refreshTokenKey) || ''; this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null'); } - // Getters + // ========== Getters ========== get token() { return this._token; } get refreshToken() { return this._refreshToken; } get user() { return this._user; } - // Setters + // ========== Setters ========== set token(val) { this._token = val; if (val) localStorage.setItem(this.tokenKey, val); @@ -45,7 +54,7 @@ class VBase { else localStorage.removeItem(this.userKey); } - // API Helpers + // ========== API 请求 ========== async request(method, path, data = null, headers = {}) { const url = `${this.baseURL}${path}`; const config = { @@ -62,7 +71,6 @@ class VBase { const resData = await response.json(); if (!response.ok) { - // Include resData in the error so caller can access the response body const error = new Error(resData.message || `Request failed: ${response.status}`); Object.assign(error, resData); throw error; @@ -75,35 +83,27 @@ class VBase { return resData.data || resData; } - // Auth Actions + // ========== 认证相关 ========== async login(username, password) { - try { - 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; // Set user directly from login response - return true; - } - return false; - } catch (e) { - throw e; + 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; + return true; } + return false; } async logout(redirect) { try { - // Optional: Call server logout // await this.request('POST', '/api/auth/logout'); } catch (e) { console.warn('Logout API failed', e); } finally { this.clear(); - if (redirect) { - location.href = redirect; - } else { - location.reload(); - } + if (redirect) location.href = redirect; + else location.reload(); } } @@ -129,145 +129,116 @@ class VBase { return user; } - // Auth Headers getAuthHeaders() { const headers = {}; - if (this.token) { - headers['Authorization'] = `Bearer ${this.token}`; - } + if (this.token) headers['Authorization'] = `Bearer ${this.token}`; return headers; } - // 检查是否有全局管理员权限 (*:* 或 scope:*:*) - _isAdmin() { - const perms = this.user?.permissions || []; - for (const p of perms) { - const permID = p.permission_id || p; - const resourceID = p.resource_id || '*'; - // 必须是全局权限 (resource_id 为 * 或空) 且是通配符权限 - if (resourceID === '*' && (permID === '*:*' || permID === `${this.scope}:*:*`)) { - return true; - } - } - return false; + clear() { + this.token = ''; + this.refreshToken = ''; + this.user = null; } - // Permission Check - // 基础权限检查 (permissionID 格式: "resource:action") - checkPerm(permissionID) { - if (!this.user) return false; - if (!permissionID) return true; - // 全局管理员直接通过 - if (this._isAdmin()) return true; - - const perms = this.user.permissions || []; - for (const p of perms) { - if (this._matchPermission(p.permission_id || p, permissionID)) { - return true; - } + 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; } - return false; } - // 检查对特定资源的权限 - // permissionID: "resource:action" - // resourceID: 资源实例ID - checkPermOnResource(permissionID, resourceID) { + // ========== 权限检查 (与后端接口一致) ========== + + /** + * 检查权限 + * @param {string} code - 权限码,如 "resource:instance" + * @param {number} level - 需要的权限等级 + * @returns {boolean} + */ + Perm(code, level = Level.Read) { if (!this.user) return false; - if (!permissionID) return true; - // 全局管理员直接通过 - if (this._isAdmin()) return true; - if (!resourceID) return this.checkPerm(permissionID); + if (!code) return true; const perms = this.user.permissions || []; - for (const p of perms) { - const permID = p.permission_id || p; - const permResourceID = p.resource_id || '*'; + return this._checkPermissionLevel(perms, code, level); + } - if (this._matchPermission(permID, permissionID)) { - if (permResourceID === '*' || permResourceID === resourceID) { - return true; - } - } - } - return false; + /** + * 检查创建权限 (level 1,检查奇数层) + * @param {string} code - 权限码 + * @returns {boolean} + */ + PermCreate(code) { + return this.Perm(code, Level.Create); } - // 满足任一权限即可 - checkPermAny(...permissionIDs) { - for (const pid of permissionIDs) { - if (this.checkPerm(pid)) return true; - } - return false; + /** + * 检查读取权限 (level 2,检查偶数层) + * @param {string} code - 权限码 + * @returns {boolean} + */ + PermRead(code) { + return this.Perm(code, Level.Read); } - // 满足所有权限 - checkPermAll(...permissionIDs) { - for (const pid of permissionIDs) { - if (!this.checkPerm(pid)) return false; - } - return true; + /** + * 检查更新权限 (level 4,检查偶数层) + * @param {string} code - 权限码 + * @returns {boolean} + */ + PermWrite(code) { + return this.Perm(code, Level.Write); } - // 内部方法: 权限匹配 - // 支持通配符: *:*, resource:* - // 后端权限可能是 "scope:resource:action" 或 "resource:action" - _matchPermission(storedPerm, wantPerm) { - if (storedPerm === wantPerm) return true; - if (storedPerm === '*:*') return true; - - // 从后端存储的权限中提取 resource:action 部分 - let havePerm = storedPerm; - const haveParts = storedPerm.split(':'); - if (haveParts.length === 3) { - // scope:resource:action -> resource:action - havePerm = `${haveParts[1]}:${haveParts[2]}`; - } + /** + * 检查管理员权限 (level 7,检查偶数层) + * @param {string} code - 权限码 + * @returns {boolean} + */ + PermAdmin(code) { + return this.Perm(code, Level.Admin); + } - if (havePerm === wantPerm) return true; + // ========== 内部方法 ========== - // 通配符匹配 (resource:* 匹配 resource:any_action) - const [haveRes, haveAct] = havePerm.split(':'); - const [wantRes, wantAct] = wantPerm.split(':'); + /** + * 核心权限检查逻辑 (与后端 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; - if (haveRes === '*' || (haveRes === wantRes && (haveAct === '*' || haveAct === wantAct))) { - return true; - } - return false; - } + // 1. 管理员特权 (Level 7 且是父级或同级) + if (permLevel === Level.Admin) { + if (permID === '*' || targetPermID.startsWith(permID + ':') || permID === targetPermID) { + return true; + } + } - hasRole(role) { - if (!this.user) return false; - // 全局管理员拥有所有角色 - if (this._isAdmin()) return true; - const userRoles = this.user.roles || []; - return userRoles.includes(role); - } + // 2. 普通权限匹配 + if (permLevel >= requiredLevel) { + if (permID === targetPermID) { + return true; + } + } + } - // State Management - clear() { - this.token = ''; - this.refreshToken = ''; - this.user = null; - this.currentOrg = null; + return false; } - 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; - } - } + // ========== Axios 集成 ========== wrapAxios(axiosInstance) { - // Request Interceptor + // 请求拦截器 axiosInstance.interceptors.request.use(config => { const headers = this.getAuthHeaders(); for (const key in headers) { @@ -276,26 +247,26 @@ class VBase { return config; }, error => Promise.reject(error)); - // Response Interceptor - axiosInstance.interceptors.response.use(response => { - const res = response.data; - return res || 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('/login?redirect=' + encodeURIComponent(window.location.pathname)); - return Promise.reject(e); + // 响应拦截器 + axiosInstance.interceptors.response.use( + response => response.data || 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('/login?redirect=' + encodeURIComponent(window.location.pathname)); + return Promise.reject(e); + } } + return Promise.reject(error?.response?.data || error); } - return Promise.reject(error?.response?.data || error); - }); + ); } }