feat: Frontend gap closure — Schedules, Alerts, Migration, Changelog views
Implements missing frontend views and API integrations: New Views: - SchedulesView: CRUD for scheduled tasks (restart/announcement/command/plugin_reload) - MigrationView: Export/import interface with file upload and history tracking - ChangelogView: Paginated changelog feed with category badges - ForgotPasswordView: Password reset flow with email submission - AlertsView: Alert config dashboard with threshold settings and history Component Updates: - ErrorBoundary: Global error handler with retry functionality - DashboardLayout: Mobile responsive sidebar, permission-based nav, new menu items - ServerInfoView: Complete rewrite for public server info display Infrastructure: - useApi: Token refresh interceptor with 401 retry and infinite loop prevention - plugins store: Implemented all stubbed methods with real API calls - auth store: Added hasPermission() helper for RBAC UI visibility - Router: Added new routes with catch-all fallback Purpose: Closes frontend implementation gaps. Hardens auth flow, improves mobile UX, enables server automation scheduling, alert configuration, and data migration tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,9 @@ interface RequestOptions {
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
let isRefreshing = false
|
||||
let refreshPromise: Promise<void> | null = null
|
||||
|
||||
/**
|
||||
* Composable for making authenticated API requests.
|
||||
* Automatically attaches JWT token and handles token refresh.
|
||||
@@ -15,6 +18,43 @@ interface RequestOptions {
|
||||
export function useApi() {
|
||||
const auth = useAuthStore()
|
||||
|
||||
async function attemptRefresh(): Promise<void> {
|
||||
if (isRefreshing && refreshPromise) {
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: auth.refreshToken }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Refresh failed')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
auth.setAuth({
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
user: auth.user!,
|
||||
})
|
||||
} catch {
|
||||
auth.logout()
|
||||
window.location.href = '/login'
|
||||
throw new Error('Session expired')
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -25,17 +65,22 @@ export function useApi() {
|
||||
headers['Authorization'] = `Bearer ${auth.accessToken}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
let response = await fetch(`${API_BASE}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
})
|
||||
|
||||
if (response.status === 401) {
|
||||
// TODO: Attempt token refresh, retry, or redirect to login
|
||||
auth.logout()
|
||||
window.location.href = '/login'
|
||||
throw new Error('Unauthorized')
|
||||
if (response.status === 401 && auth.refreshToken) {
|
||||
await attemptRefresh()
|
||||
|
||||
// Retry original request with new token
|
||||
headers['Authorization'] = `Bearer ${auth.accessToken}`
|
||||
response = await fetch(`${API_BASE}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user