Files
Infrastruktur-Monitoring-Sy…/frontend/src/api/client.ts
Dennis Nemec f1136fdf3d
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled
WP-01: authentication, user management and application shell
Argon2id password hashing, JWT access tokens with rotating HttpOnly refresh
cookies and reuse detection, login rate limiting, auth audit log, bootstrap
admin, admin-only user CRUD and password reset, OpenAPI spec. Vue login page,
auth store with automatic token refresh, route guards, sidebar shell with
toasts and placeholder pages, user management page.

All tests green: 40 backend, 12 Vitest, 4 Playwright.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 21:37:14 +02:00

63 lines
2.1 KiB
TypeScript

import { useAuthStore } from '../stores/auth'
import type { TokenResponse } from './types'
export class ApiError extends Error {
status: number
code: string
constructor(status: number, code: string, message: string) {
super(message)
this.status = status
this.code = code
}
}
async function parse(res: Response): Promise<unknown> {
if (res.status === 204) return undefined
const text = await res.text()
return text ? JSON.parse(text) : undefined
}
async function toError(res: Response): Promise<ApiError> {
const body = (await parse(res).catch(() => undefined)) as
{ error?: string; message?: string } | undefined
return new ApiError(res.status, body?.error ?? 'unknown', body?.message ?? res.statusText)
}
/** Refresh the access token via the HttpOnly cookie. Returns false if it failed. */
export async function refreshAccessToken(): Promise<boolean> {
const auth = useAuthStore()
const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'same-origin' })
if (!res.ok) {
auth.clear()
return false
}
const data = (await parse(res)) as TokenResponse
auth.setSession(data)
return true
}
async function request<T>(method: string, path: string, body?: unknown, retry = true): Promise<T> {
const auth = useAuthStore()
const headers: Record<string, string> = {}
if (body !== undefined) headers['Content-Type'] = 'application/json'
if (auth.accessToken) headers['Authorization'] = `Bearer ${auth.accessToken}`
const res = await fetch(path, {
method,
headers,
credentials: 'same-origin',
body: body === undefined ? undefined : JSON.stringify(body),
})
if (res.status === 401 && retry && !path.startsWith('/api/auth/')) {
if (await refreshAccessToken()) return request<T>(method, path, body, false)
throw new ApiError(401, 'unauthorized', 'session expired')
}
if (!res.ok) throw await toError(res)
return (await parse(res)) as T
}
export const api = {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
}