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

347 lines
9.2 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 接口
*/
// 权限等级常量 (与后端一致)
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');
if (this._token) {
this.fetchUser()
}
}
// ========== 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
this.fetchUser()
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;
this.fetchUser()
}
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;
this.fetchUser()
}
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;
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);
}
}
async refresh() {
if (!this.refreshToken) return false;
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) {
console.warn(e)
return false
}
}
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;