mirror of https://github.com/veypi/OneAuth.git
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.
218 lines
6.7 KiB
JavaScript
218 lines
6.7 KiB
JavaScript
|
|
class VBase {
|
|
constructor(baseURL) {
|
|
this.baseURL = baseURL || '';
|
|
this.tokenKey = 'vbase_access_token';
|
|
this.refreshTokenKey = 'vbase_refresh_token';
|
|
this.userKey = 'vbase_user_info';
|
|
this.orgKey = 'vbase_current_org';
|
|
|
|
this._token = localStorage.getItem(this.tokenKey) || '';
|
|
this._refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
|
|
this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null');
|
|
this._currentOrg = JSON.parse(localStorage.getItem(this.orgKey) || 'null');
|
|
}
|
|
|
|
// Getters
|
|
get token() { return this._token; }
|
|
get refreshToken() { return this._refreshToken; }
|
|
get user() { return this._user; }
|
|
get currentOrg() { return this._currentOrg; }
|
|
|
|
// 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);
|
|
}
|
|
|
|
set currentOrg(val) {
|
|
this._currentOrg = val;
|
|
if (val) localStorage.setItem(this.orgKey, JSON.stringify(val));
|
|
else localStorage.removeItem(this.orgKey);
|
|
}
|
|
|
|
// API Helpers
|
|
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) {
|
|
throw resData || new Error(`Request failed: ${response.status}`);
|
|
}
|
|
|
|
if (resData.code && resData.code !== 200) {
|
|
throw new Error(resData.message || 'API Error');
|
|
}
|
|
|
|
return resData.data || resData;
|
|
}
|
|
|
|
// Auth Actions
|
|
async login(username, password) {
|
|
try {
|
|
const data = await this.request('POST', '/api/auth/login', { username, password });
|
|
if (data.access) {
|
|
this.token = data.access;
|
|
if (data.refresh) this.refreshToken = data.refresh;
|
|
await this.fetchUser();
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async logout(redirect) {
|
|
try {
|
|
// Optional: Call server logout
|
|
// await this.request('POST', '/api/auth/logout');
|
|
} catch (e) {
|
|
console.warn('Logout API failed', e);
|
|
} finally {
|
|
this.clear();
|
|
if (redirect) {
|
|
location.href = redirect;
|
|
} else {
|
|
location.reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
async refresh() {
|
|
if (!this.refreshToken) throw new Error("No refresh token");
|
|
try {
|
|
const data = await this.request('POST', '/api/auth/refresh', { refresh: this.refreshToken });
|
|
if (data.access) {
|
|
this.token = data.access;
|
|
if (data.refresh) this.refreshToken = data.refresh;
|
|
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;
|
|
}
|
|
|
|
// Auth Headers
|
|
getAuthHeaders() {
|
|
const headers = {};
|
|
if (this.token) {
|
|
headers['Authorization'] = `Bearer ${this.token}`;
|
|
}
|
|
if (this.currentOrg && this.currentOrg.id) {
|
|
headers['X-Org-ID'] = this.currentOrg.id;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
// Permission Check
|
|
hasPermission(permission) {
|
|
if (!this.user) return false;
|
|
if (this.user.is_admin) return true;
|
|
if (!permission) return true;
|
|
const userPerms = this.user.permissions || [];
|
|
return userPerms.includes(permission);
|
|
}
|
|
|
|
hasRole(role) {
|
|
if (!this.user) return false;
|
|
if (this.user.is_admin) return true;
|
|
const userRoles = this.user.roles || [];
|
|
return userRoles.includes(role);
|
|
}
|
|
|
|
// State Management
|
|
clear() {
|
|
this.token = '';
|
|
this.refreshToken = '';
|
|
this.user = null;
|
|
this.currentOrg = 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;
|
|
}
|
|
}
|
|
|
|
wrapAxios(axiosInstance) {
|
|
// Request Interceptor
|
|
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));
|
|
|
|
// Response Interceptor
|
|
axiosInstance.interceptors.response.use(response => {
|
|
const res = response.data;
|
|
if (res && res.code === 200) {
|
|
return res.data;
|
|
}
|
|
if (res && res.code && res.code !== 200) {
|
|
return Promise.reject(new Error(res.message || 'Error'));
|
|
}
|
|
return res || 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(window.location.pathname);
|
|
return Promise.reject(e);
|
|
}
|
|
}
|
|
return Promise.reject(error?.response?.data || error);
|
|
});
|
|
}
|
|
}
|
|
|
|
export default VBase;
|