Backend: - AppConfig, AppError, AppState modules for shared infrastructure - JWT creation/verification with HS256 (jsonwebtoken crate) - Session management: SHA-256 token hashing, DB-backed sessions - Auth middleware: AuthUser, RequireHost, RequireAdmin extractors - POST /api/v1/join: name-only registration, 4-digit PIN + bcrypt hash - POST /api/v1/recover: PIN-based recovery with 3-attempt lockout (15 min) - POST /api/v1/admin/login: bcrypt password verification - DELETE /api/v1/session: logout (session invalidation) - Migration 006: user PIN lockout columns (failed_pin_attempts, pin_locked_until) - Models: Event, User (with role enum), Session with all CRUD methods Frontend: - api.ts: typed fetch wrapper with automatic Bearer token injection - auth.ts: JWT/PIN localStorage management with Svelte store - /join: name entry form with PIN display modal and copy button - /recover: name + PIN recovery form with saved PIN pre-fill - /feed: placeholder gallery page with logout - Root layout: auth initialization on mount - Root page: redirect to /join or /feed based on auth state All responses use German language strings as specified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
58 lines
1.2 KiB
TypeScript
58 lines
1.2 KiB
TypeScript
import { getToken, clearAuth } from './auth';
|
|
|
|
const BASE = '/api/v1';
|
|
|
|
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 request<T>(
|
|
method: string,
|
|
path: string,
|
|
body?: unknown
|
|
): Promise<T> {
|
|
const headers: Record<string, string> = {};
|
|
const token = getToken();
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
if (body !== undefined) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
});
|
|
|
|
if (res.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 401) {
|
|
clearAuth();
|
|
}
|
|
throw new ApiError(res.status, data.error ?? 'unknown', data.message ?? 'Fehler');
|
|
}
|
|
|
|
return data 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),
|
|
delete: <T>(path: string) => request<T>('DELETE', path)
|
|
};
|