diff --git a/api/org/list.go b/api/org/list.go index 033579b..76e6ddb 100644 --- a/api/org/list.go +++ b/api/org/list.go @@ -13,7 +13,7 @@ import ( type ListRequest struct { Page int `json:"page" src:"query" default:"1"` PageSize int `json:"page_size" src:"query" default:"20"` - Keyword string `json:"keyword" src:"query"` + Keyword string `json:"keyword" src:"query" default:""` } type ListResponse struct { diff --git a/ui/env.js b/ui/env.js index c0add29..51d44bd 100644 --- a/ui/env.js +++ b/ui/env.js @@ -28,7 +28,7 @@ export default async ($env) => { try { await vbase.refresh(); } catch (e) { - vbase.logout(to.fullPath); + vbase.logout('/login?redirect=' + encodeURIComponent(to.fullPath)); return false; } } @@ -37,7 +37,7 @@ export default async ($env) => { try { await vbase.fetchUser(); } catch (e) { - vbase.logout(to.fullPath); + vbase.logout('/login?redirect=' + encodeURIComponent(to.fullPath)); return false; } } @@ -50,11 +50,6 @@ export default async ($env) => { return false; } } - } else if (isGuest) { - if (!vbase.isExpired()) { - next('/'); - return false; - } } next(); diff --git a/ui/layout/default.html b/ui/layout/default.html index e6ebb58..8803cc6 100644 --- a/ui/layout/default.html +++ b/ui/layout/default.html @@ -157,12 +157,12 @@ // Define Menu Items menuItems = [ - {label: $t('nav.dashboard'), icon: "", path: "/"}, - {label: $t('nav.org'), icon: "", path: "/org"}, - {label: $t('nav.profile'), icon: "", path: "/profile"}, + {label: () => $t('nav.dashboard'), icon: "", path: "/"}, + {label: () => $t('nav.org'), icon: "", path: "/org"}, + {label: () => $t('nav.profile'), icon: "", path: "/profile"}, // Admin only items would be filtered here ideally - {label: $t('nav.users'), icon: "", path: "/users"}, - {label: $t('nav.oauth'), icon: "", path: "/oauth/apps"}, + {label: () => $t('nav.users'), icon: "", path: "/users"}, + {label: () => $t('nav.oauth'), icon: "", path: "/oauth/apps"}, ]; currentRouteName = ""; @@ -186,34 +186,12 @@ }; diff --git a/ui/page/auth/login.html b/ui/page/auth/login.html index af37257..e16434f 100644 --- a/ui/page/auth/login.html +++ b/ui/page/auth/login.html @@ -73,7 +73,6 @@ - 123

{{ $t('auth.login') }}

{{ error }}
@@ -106,7 +105,8 @@ error = ""; try { await $env.$vbase.login(username, password); - $router.push('/'); + const redirect = $router.query.redirect || '/'; + $router.push(redirect); } catch (err) { error = err.message || "Login failed"; } diff --git a/ui/page/dashboard/index.html b/ui/page/dashboard/index.html index deb6a8e..549e9fb 100644 --- a/ui/page/dashboard/index.html +++ b/ui/page/dashboard/index.html @@ -1,115 +1,123 @@ + - - {{ $t('nav.dashboard') }} - + + {{ $t('nav.dashboard') }} + + -
-

{{ $t('nav.dashboard') }}

- -
-
-
{{ stat.title }}
-
{{ stat.value }}
-
-
- -
+
+

{{ $t('nav.dashboard') }}

+ +
+
+
{{ stat.title }}
+
{{ stat.value }}
+
+ +
+
+ diff --git a/ui/routes.js b/ui/routes.js index 42d907d..9bafc9c 100644 --- a/ui/routes.js +++ b/ui/routes.js @@ -1,7 +1,7 @@ const routes = [ // Public - { path: '/login', component: '/page/auth/login.html', layout: 'public', meta: { guest: true } }, - { path: '/register', component: '/page/auth/register.html', layout: 'public', meta: { guest: true } }, + { path: '/login', component: '/page/auth/login.html', layout: 'public' }, + { path: '/register', component: '/page/auth/register.html', layout: 'public' }, // Dashboard (Default Layout) { diff --git a/ui/vbase.js b/ui/vbase.js index e50366e..0c2b89a 100644 --- a/ui/vbase.js +++ b/ui/vbase.js @@ -1,217 +1,220 @@ 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) { + 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 { - 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; + await this.refresh(); + const headers = this.getAuthHeaders(); + originalRequest.headers['Authorization'] = headers['Authorization']; + return axiosInstance(originalRequest); } catch (e) { - throw e; + this.logout('/login?redirect=' + encodeURIComponent(window.location.pathname)); + return Promise.reject(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); - }); - } + } + return Promise.reject(error?.response?.data || error); + }); + } } export default VBase;