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>
112 lines
2.8 KiB
TypeScript
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 }
|
|
}
|