You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/vbase.js

346 lines
8.9 KiB
JavaScript

/**
* 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;
// 初始化鉴权:有用户缓存就验证 + 定时刷新
if (this._user) {
this._ensureAuth();
}
}
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) {
if (!this.user) return false;
if (!code) return true;
const perms = this.user.permissions || [];
return this._checkPermissionLevel(perms, code, level);
}
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(perms, targetPermID, requiredLevel) {
for (const p of perms) {
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;