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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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;