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

302 lines
7.6 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 管理员 (完全控制)
};
class VBase {
constructor(scope, baseURL, login_page, users = {}) {
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.userKey = 'vbase_user';
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;
// 验证登录状态
this.fetchUser().catch(() => {});
}
// ========== 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);
}
this._cachePublicUser(val);
}
// ========== 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;
}
// ========== 认证 ==========
/** 用户名密码登录 */
async login(username, password) {
const data = await this.request('POST', '/api/auth/login', { username, password });
if (data.user) {
this.user = data.user;
await this.fetchUser();
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) {
this.user = data.user;
await this.fetchUser();
}
return data;
}
/** 绑定已有账号 */
async bindAccount(tempToken, username, password) {
const data = await this.request('POST', '/api/auth/bind', {
temp_token: tempToken,
username,
password,
});
if (data.user) {
this.user = data.user;
await this.fetchUser();
}
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) {
this.user = data.user;
await this.fetchUser();
}
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', {});
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.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;