/** * 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, login_page) { if (!scope) throw new Error('VBase: scope is required'); if (!baseURL) baseURL = window.location.origin; if (baseURL === '' || baseURL === '/') baseURL = window.location.origin; this.baseURL = baseURL; this.scope = scope; this.login_page = login_page || (baseURL + '/login') 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 ========== 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); } set refreshToken(val) { this._refreshToken = val; if (val) localStorage.setItem(this.refreshTokenKey, val); else localStorage.removeItem(this.refreshTokenKey); } set user(val) { this._user = val; if (val) localStorage.setItem(this.userKey, JSON.stringify(val)); else localStorage.removeItem(this.userKey); } // ========== API 请求 ========== async request(method, path, data = null, headers = {}) { const url = `${this.baseURL}${path}`; const config = { method, headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders(), ...headers } }; if (data) config.body = JSON.stringify(data); const response = await fetch(url, config); const resData = await response.json(); if (!response.ok) { const error = new Error(resData.message || `Request failed: ${response.status}`); Object.assign(error, resData); throw error; } if (resData.code && resData.code !== 200) { throw new Error(resData.message || 'API 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; return true; } return false; } /** * OAuth Callback Handler * @param {string} provider * @param {string} code * @param {string} state * @returns {Promise} Response data */ 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; } return data; } /** * Bind existing account * @param {string} tempToken * @param {string} username * @param {string} password * @returns {Promise} */ async bindAccount(tempToken, username, password) { const data = await this.request('POST', '/api/auth/bind', { temp_token: tempToken, username, password }); 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; } return data; } /** * Register and bind new account * @param {string} tempToken * @param {string} username * @param {string} email * @returns {Promise} */ async bindRegister(tempToken, username, email) { const data = await this.request('POST', '/api/auth/bind-register', { temp_token: tempToken, username, 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; } return data; } async logout(redirect) { try { await this.request('POST', '/api/auth/logout'); } catch (e) { console.warn('Logout API failed', e); } finally { this.clear(); const redirectUrl = redirect || window.location.pathname + window.location.search; location.href = this.login_page + '?redirect=' + encodeURIComponent(redirectUrl); } } async refresh() { if (!this.refreshToken) throw new Error("No refresh token"); 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; return true; } return false; } catch (e) { this.logout(); throw e; } } async fetchUser() { const user = await this.request('GET', '/api/auth/me'); this.user = user; return user; } getAuthHeaders() { const headers = {}; if (this.token) headers['Authorization'] = `Bearer ${this.token}`; return headers; } clear() { this.token = ''; this.refreshToken = ''; this.user = null; } 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; const perms = this.user.permissions || []; 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); } // ========== 内部方法 ========== /** * 核心权限检查逻辑 (与后端 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); } ); } } export default VBase;