/** * VBase - Scoped RBAC 权限管理客户端 * 对应后端: github.com/veypi/vigo/contrib/auth.Auth 接口 * * Token 基于 HttpOnly Cookie 管理,JS 不可读,防沙箱泄露。 * 非浏览器客户端仍可通过 Authorization header 传递 token。 */ // 权限等级常量 (与后端一致) export const Level = { None: 0, Create: 1, // 001 创建 (奇数层) Read: 2, // 010 读取 (偶数层) Write: 4, // 100 写入 (偶数层) ReadWrite: 6, // 110 读写 (偶数层) Admin: 7, // 111 管理员 (完全控制) }; const REFRESH_INTERVAL = 12 * 60 * 1000; // 12 分钟(access_token 15min 过期) class VBase { constructor(baseURL, login_page, users = {}) { if (!baseURL) baseURL = window.location.origin; if (baseURL === '' || baseURL === '/') baseURL = window.location.origin; this.baseURL = baseURL; this.login_page = login_page || (baseURL + '/login'); this.userKey = 'vbase_user'; this.refreshTimer = null; this.users = users; 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; } /** 异步判断是否已登录 */ async isLogin() { if (!this._user) return false; if (!this._initDone) { this._initDone = true; await this._ensureAuth(); } else { const now = Date.now(); const lastRefresh = parseInt(localStorage.getItem('vbase_last_refresh'), 10) || 0; if (now - lastRefresh > REFRESH_INTERVAL) { const ok = await this.refresh(); if (!ok) { this.clear(); return false; } } } return !!this._user; } async _ensureAuth() { const now = Date.now(); const lastRefresh = parseInt(localStorage.getItem('vbase_last_refresh'), 10) || 0; // 超过 12 分钟没刷新,先刷新再拉用户 if (now - lastRefresh > REFRESH_INTERVAL) { const ok = await this.refresh(); if (!ok) { this.clear(); return; } } // 无用户信息或刷新后重新拉 try { await this.fetchUser(); } catch (e) { this.clear(); return; } // 启动定时刷新 this._startRefreshTimer(); } _startRefreshTimer() { this._stopRefreshTimer(); this.refreshTimer = setInterval(() => { this.refresh().then(ok => { if (!ok) this.clear(); }); }, REFRESH_INTERVAL); } _stopRefreshTimer() { if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null; } } // ========== 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); localStorage.removeItem('vbase_last_refresh'); } this._cachePublicUser(val); } _touchRefresh() { localStorage.setItem('vbase_last_refresh', Date.now().toString()); } // ========== API 请求 ========== async request(method, path, data = null, headers = {}) { const url = `${this.baseURL}${path}`; const config = { method, headers: { 'Content-Type': 'application/json', ...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) { const error = new Error(resData.message || 'API Error'); Object.assign(error, resData); throw error; } return resData.data || resData; } // ========== 认证 ========== /** 登录成功后初始化鉴权状态(密码登录之外的方式:验证码、OAuth、注册后自动登录等) */ async onAuthSuccess(user) { this._touchRefresh(); this.user = user; await this.fetchUser(); this._startRefreshTimer(); } /** 用户名密码登录 */ async login(username, password) { const data = await this.request('POST', '/api/auth/login', { username, password }); if (data.user) { await this.onAuthSuccess(data.user); return true; } return false; } /** OAuth 回调 */ async oauthCallback(provider, code, state) { const data = await this.request('GET', `/api/auth/callback/${provider}?code=${code}&state=${state}`); if (data.user) await this.onAuthSuccess(data.user); return data; } /** 绑定已有账号 */ async bindAccount(tempToken, username, password) { const data = await this.request('POST', '/api/auth/bind', { temp_token: tempToken, username, password, }); if (data.user) await this.onAuthSuccess(data.user); return data; } /** 绑定并注册 */ async bindRegister(tempToken, username, email) { const data = await this.request('POST', '/api/auth/bind-register', { temp_token: tempToken, username, email, }); if (data.user) await this.onAuthSuccess(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); } } /** Token 刷新 */ async refresh() { try { await this.request('POST', '/api/auth/refresh', {}); this._touchRefresh(); return true; } catch (e) { return false; } } /** 获取当前用户信息及权限 */ async fetchUser() { try { const user = await this.request('GET', '/api/auth/me'); this.user = user; return user; } catch (e) { this.clear(); throw e; } } /** 清除登录状态 */ clear() { this._stopRefreshTimer(); this.user = null; for (const id of Object.keys(this.users)) { delete this.users[id]; } this._pendingUserIDs.clear(); this._loadingUserIDs.clear(); this._resolvedUserIDs.clear(); this._pendingUserFlush = null; } // ========== 用户 ========== User(id) { if (!id) return {}; if (!this.users[id]) { this.users[id] = {}; } if (!this._resolvedUserIDs.has(id) && !this._loadingUserIDs.has(id)) { this._pendingUserIDs.add(id); if (!this._pendingUserFlush) { this._pendingUserFlush = Promise.resolve().then(() => this._flushUserRequests()); } } return this.users[id]; } // ========== 权限检查 ========== Perm(code, level = Level.Read, scope) { if (!this.user) return false; if (!code) return true; const perms = this.user.permissions || []; return this._checkPermissionLevel(perms, code, level, scope); } PermCreate(code, scope) { return this.Perm(code, Level.Create, scope); } PermRead(code, scope) { return this.Perm(code, Level.Read, scope); } PermWrite(code, scope) { return this.Perm(code, Level.Write, scope); } PermAdmin(code, scope) { return this.Perm(code, Level.Admin, scope); } // ========== 内部方法 ========== _checkPermissionLevel(perms, targetPermID, requiredLevel, scope) { for (const p of perms) { if (scope && p.scope !== scope) continue; const permID = p.permission_id || p; const permLevel = p.level !== undefined ? p.level : Level.Read; if (permLevel === Level.Admin) { if (permID === '*' || targetPermID.startsWith(permID + ':') || permID === targetPermID) { return true; } } if (permLevel >= requiredLevel) { if (permID === targetPermID) { return true; } } } return false; } _cachePublicUser(user) { if (!user?.id) return; const cached = { ...user, name: user.name || user.nickname || user.username || '', icon: user.icon || user.avatar || '', avatar: user.avatar || user.icon || '', }; if (!this.users[cached.id]) { this.users[cached.id] = {}; } Object.assign(this.users[cached.id], cached); this._resolvedUserIDs.add(cached.id); this._loadingUserIDs.delete(cached.id); } async _flushUserRequests() { const ids = [...this._pendingUserIDs].filter(Boolean); this._pendingUserIDs.clear(); this._pendingUserFlush = null; if (ids.length === 0) return; for (const id of ids) { this._loadingUserIDs.add(id); } try { const res = await this.request('POST', '/api/auth/users', { ids }); const items = Array.isArray(res?.items) ? res.items : []; const found = new Set(); for (const item of items) { this._cachePublicUser(item); found.add(item.id); } for (const id of ids) { this._loadingUserIDs.delete(id); if (!found.has(id)) { this._resolvedUserIDs.add(id); } } } catch (error) { for (const id of ids) { this._loadingUserIDs.delete(id); } console.warn('VBase: batch fetch users failed', error); } } } export default VBase;