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

341 lines
9.1 KiB
JavaScript

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