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) { // Include resData in the error so caller can access the response body 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; } // Auth Actions async login(username, password) { try { 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; // Set user directly from login response 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_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; } // 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('/login?redirect=' + encodeURIComponent(window.location.pathname)); return Promise.reject(e); } } return Promise.reject(error?.response?.data || error); }); } } export default VBase;