Files
corrosion-admin-panel/frontend/src/composables/useApi.ts
Vantz Stockwell 4c648783a2 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>
2026-02-15 21:20:40 -05:00

112 lines
2.8 KiB
TypeScript

import { useAuthStore } from '@/stores/auth'
const API_BASE = '/api'
interface RequestOptions {
method?: string
body?: unknown
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.
*/
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',
...options.headers,
}
if (auth.accessToken) {
headers['Authorization'] = `Bearer ${auth.accessToken}`
}
let response = await fetch(`${API_BASE}${path}`, {
method: options.method || 'GET',
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
})
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) {
const error = await response.json().catch(() => ({ message: 'Request failed' }))
throw new Error(error.message || `HTTP ${response.status}`)
}
return response.json()
}
function get<T>(path: string) {
return request<T>(path)
}
function post<T>(path: string, body?: unknown) {
return request<T>(path, { method: 'POST', body })
}
function put<T>(path: string, body?: unknown) {
return request<T>(path, { method: 'PUT', body })
}
function del<T>(path: string) {
return request<T>(path, { method: 'DELETE' })
}
return { request, get, post, put, del }
}