refactor(ui): Update vbase.js to match new Scoped RBAC permission system

- Replace old permission check methods with new Perm/PermCreate/PermRead/PermWrite/PermAdmin
    - Add Level constants export (None, Create, Read, Write, ReadWrite, Admin)
    - Remove role-based permission checks (hasRole, checkPermAny, checkPermAll)
    - Update core permission checking logic to match backend checkPermissionLevel
    - Remove _isAdmin helper, use Level.Admin check instead
    - Simplify localStorage keys (remove scope prefix from keys)
    - Clean up console.log in env.js
master
veypi 3 weeks ago
parent 65bd2b5b52
commit 5460289957

@ -12,7 +12,6 @@ export default async ($env) => {
// Initialize VBase Service // Initialize VBase Service
const vbase = new VBase('vb', '/'); // Relative path const vbase = new VBase('vb', '/'); // Relative path
console.log(vbase)
$env.$vbase = vbase; $env.$vbase = vbase;
// Wrap Axios // Wrap Axios
@ -21,7 +20,6 @@ export default async ($env) => {
// Router Guard // Router Guard
$env.$router.beforeEnter = async (to, from, next) => { $env.$router.beforeEnter = async (to, from, next) => {
const isAuth = to.meta && to.meta.auth; 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 const roles = to.meta && to.meta.roles; // Array of required roles
if (isAuth) { if (isAuth) {

@ -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 { class VBase {
constructor(scope, baseURL) { constructor(scope, baseURL) {
if (!scope) throw new Error('VBase: scope is required'); if (!scope) throw new Error('VBase: scope is required');
if (!baseURL) throw new Error('VBase: baseURL is required'); if (!baseURL) baseURL = window.location.origin;
if (!baseURL) { if (baseURL === '' || baseURL === '/') baseURL = window.location.origin;
baseURL = ''
}
if (baseURL === '' || baseURL === '/') {
baseURL = window.location.origin
}
this.baseURL = baseURL; this.baseURL = baseURL;
this.scope = scope; this.scope = scope;
this.tokenKey = 'vbase_access_token'; this.tokenKey = `vbase_token`;
this.refreshTokenKey = 'vbase_refresh_token'; this.refreshTokenKey = `vbase_refresh_token`;
this.userKey = 'vbase_user_info'; this.userKey = `vbase_user`;
this._token = localStorage.getItem(this.tokenKey) || ''; this._token = localStorage.getItem(this.tokenKey) || '';
this._refreshToken = localStorage.getItem(this.refreshTokenKey) || ''; this._refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null'); this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null');
} }
// Getters // ========== Getters ==========
get token() { return this._token; } get token() { return this._token; }
get refreshToken() { return this._refreshToken; } get refreshToken() { return this._refreshToken; }
get user() { return this._user; } get user() { return this._user; }
// Setters // ========== Setters ==========
set token(val) { set token(val) {
this._token = val; this._token = val;
if (val) localStorage.setItem(this.tokenKey, val); if (val) localStorage.setItem(this.tokenKey, val);
@ -45,7 +54,7 @@ class VBase {
else localStorage.removeItem(this.userKey); else localStorage.removeItem(this.userKey);
} }
// API Helpers // ========== API 请求 ==========
async request(method, path, data = null, headers = {}) { async request(method, path, data = null, headers = {}) {
const url = `${this.baseURL}${path}`; const url = `${this.baseURL}${path}`;
const config = { const config = {
@ -62,7 +71,6 @@ class VBase {
const resData = await response.json(); const resData = await response.json();
if (!response.ok) { 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}`); const error = new Error(resData.message || `Request failed: ${response.status}`);
Object.assign(error, resData); Object.assign(error, resData);
throw error; throw error;
@ -75,35 +83,27 @@ class VBase {
return resData.data || resData; return resData.data || resData;
} }
// Auth Actions // ========== 认证相关 ==========
async login(username, password) { async login(username, password) {
try { const data = await this.request('POST', '/api/auth/login', { username, password });
const data = await this.request('POST', '/api/auth/login', { username, password }); if (data.access_token) {
if (data.access_token) { this.token = data.access_token;
this.token = data.access_token; if (data.refresh_token) this.refreshToken = data.refresh_token;
if (data.refresh_token) this.refreshToken = data.refresh_token; this.user = data.user;
this.user = data.user; // Set user directly from login response return true;
return true;
}
return false;
} catch (e) {
throw e;
} }
return false;
} }
async logout(redirect) { async logout(redirect) {
try { try {
// Optional: Call server logout
// await this.request('POST', '/api/auth/logout'); // await this.request('POST', '/api/auth/logout');
} catch (e) { } catch (e) {
console.warn('Logout API failed', e); console.warn('Logout API failed', e);
} finally { } finally {
this.clear(); this.clear();
if (redirect) { if (redirect) location.href = redirect;
location.href = redirect; else location.reload();
} else {
location.reload();
}
} }
} }
@ -129,145 +129,116 @@ class VBase {
return user; return user;
} }
// Auth Headers
getAuthHeaders() { getAuthHeaders() {
const headers = {}; const headers = {};
if (this.token) { if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
headers['Authorization'] = `Bearer ${this.token}`;
}
return headers; return headers;
} }
// 检查是否有全局管理员权限 (*:* 或 scope:*:*) clear() {
_isAdmin() { this.token = '';
const perms = this.user?.permissions || []; this.refreshToken = '';
for (const p of perms) { this.user = null;
const permID = p.permission_id || p;
const resourceID = p.resource_id || '*';
// 必须是全局权限 (resource_id 为 * 或空) 且是通配符权限
if (resourceID === '*' && (permID === '*:*' || permID === `${this.scope}:*:*`)) {
return true;
}
}
return false;
} }
// Permission Check isExpired(token) {
// 基础权限检查 (permissionID 格式: "resource:action") if (!token) token = this.token;
checkPerm(permissionID) { if (!token) return true;
if (!this.user) return false; try {
if (!permissionID) return true; const base64Url = token.split('.')[1];
// 全局管理员直接通过 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
if (this._isAdmin()) return true; const payload = JSON.parse(window.atob(base64));
const now = Math.floor(Date.now() / 1000);
const perms = this.user.permissions || []; return payload.exp && payload.exp < now;
for (const p of perms) { } catch (e) {
if (this._matchPermission(p.permission_id || p, permissionID)) { return true;
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 (!this.user) return false;
if (!permissionID) return true; if (!code) return true;
// 全局管理员直接通过
if (this._isAdmin()) return true;
if (!resourceID) return this.checkPerm(permissionID);
const perms = this.user.permissions || []; const perms = this.user.permissions || [];
for (const p of perms) { return this._checkPermissionLevel(perms, code, level);
const permID = p.permission_id || p; }
const permResourceID = p.resource_id || '*';
if (this._matchPermission(permID, permissionID)) { /**
if (permResourceID === '*' || permResourceID === resourceID) { * 检查创建权限 (level 1检查奇数层)
return true; * @param {string} code - 权限码
} * @returns {boolean}
} */
} PermCreate(code) {
return false; return this.Perm(code, Level.Create);
} }
// 满足任一权限即可 /**
checkPermAny(...permissionIDs) { * 检查读取权限 (level 2检查偶数层)
for (const pid of permissionIDs) { * @param {string} code - 权限码
if (this.checkPerm(pid)) return true; * @returns {boolean}
} */
return false; PermRead(code) {
return this.Perm(code, Level.Read);
} }
// 满足所有权限 /**
checkPermAll(...permissionIDs) { * 检查更新权限 (level 4检查偶数层)
for (const pid of permissionIDs) { * @param {string} code - 权限码
if (!this.checkPerm(pid)) return false; * @returns {boolean}
} */
return true; PermWrite(code) {
return this.Perm(code, Level.Write);
} }
// 内部方法: 权限匹配 /**
// 支持通配符: *:*, resource:* * 检查管理员权限 (level 7检查偶数层)
// 后端权限可能是 "scope:resource:action" 或 "resource:action" * @param {string} code - 权限码
_matchPermission(storedPerm, wantPerm) { * @returns {boolean}
if (storedPerm === wantPerm) return true; */
if (storedPerm === '*:*') return true; PermAdmin(code) {
return this.Perm(code, Level.Admin);
// 从后端存储的权限中提取 resource:action 部分 }
let havePerm = storedPerm;
const haveParts = storedPerm.split(':');
if (haveParts.length === 3) {
// scope:resource:action -> resource:action
havePerm = `${haveParts[1]}:${haveParts[2]}`;
}
if (havePerm === wantPerm) return true; // ========== 内部方法 ==========
// 通配符匹配 (resource:* 匹配 resource:any_action) /**
const [haveRes, haveAct] = havePerm.split(':'); * 核心权限检查逻辑 (与后端 checkPermissionLevel 一致)
const [wantRes, wantAct] = wantPerm.split(':'); */
_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))) { // 1. 管理员特权 (Level 7 且是父级或同级)
return true; if (permLevel === Level.Admin) {
} if (permID === '*' || targetPermID.startsWith(permID + ':') || permID === targetPermID) {
return false; return true;
} }
}
hasRole(role) { // 2. 普通权限匹配
if (!this.user) return false; if (permLevel >= requiredLevel) {
// 全局管理员拥有所有角色 if (permID === targetPermID) {
if (this._isAdmin()) return true; return true;
const userRoles = this.user.roles || []; }
return userRoles.includes(role); }
} }
// State Management return false;
clear() {
this.token = '';
this.refreshToken = '';
this.user = null;
this.currentOrg = null;
} }
isExpired(token) { // ========== Axios 集成 ==========
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;
}
}
wrapAxios(axiosInstance) { wrapAxios(axiosInstance) {
// Request Interceptor // 请求拦截器
axiosInstance.interceptors.request.use(config => { axiosInstance.interceptors.request.use(config => {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
for (const key in headers) { for (const key in headers) {
@ -276,26 +247,26 @@ class VBase {
return config; return config;
}, error => Promise.reject(error)); }, error => Promise.reject(error));
// Response Interceptor // 响应拦截器
axiosInstance.interceptors.response.use(response => { axiosInstance.interceptors.response.use(
const res = response.data; response => response.data || response,
return res || response; async error => {
}, async error => { const originalRequest = error.config;
const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) {
if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true;
originalRequest._retry = true; try {
try { await this.refresh();
await this.refresh(); const headers = this.getAuthHeaders();
const headers = this.getAuthHeaders(); originalRequest.headers['Authorization'] = headers['Authorization'];
originalRequest.headers['Authorization'] = headers['Authorization']; return axiosInstance(originalRequest);
return axiosInstance(originalRequest); } catch (e) {
} catch (e) { this.logout('/login?redirect=' + encodeURIComponent(window.location.pathname));
this.logout('/login?redirect=' + encodeURIComponent(window.location.pathname)); return Promise.reject(e);
return Promise.reject(e); }
} }
return Promise.reject(error?.response?.data || error);
} }
return Promise.reject(error?.response?.data || error); );
});
} }
} }

Loading…
Cancel
Save