nerdware/apps/web/src/lib/admin-api.ts
AyoubBenziza d3ce52b60a feat(admin): manual quiz back-office + rewrite README
Back-office (roadmap V1 step 7):
- server: token-protected HTTP API under /api/admin (ADMIN_TOKEN), Drizzle-backed
  CRUD for manual questions — list, create (mcq/truefalse/free, category upsert,
  dedup on prompt, validation), delete; 401/503 when unauthorized/unconfigured
- client: /admin page with token login + TanStack Query (list/create/delete),
  format-aware form (QCM / vrai-faux / réponse libre)
- env: ADMIN_TOKEN

README: replace the shadcn template with a real NerdWare overview (stack,
structure, getting started, scripts, game modes, back-office).

Verified against real Postgres: auth (401), create (201), dedup (409),
validation (400), list, delete (200/404).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:43:19 +02:00

65 lines
1.8 KiB
TypeScript

// Accès HTTP au back-office quiz (TanStack Query l'enrobe côté composant).
import type { QuizFormat } from "@nerdware/shared"
const SERVER_URL = import.meta.env.VITE_SERVER_URL ?? "http://localhost:3001"
const TOKEN_KEY = "nerdware-admin-token"
export function getAdminToken(): string {
return localStorage.getItem(TOKEN_KEY) ?? ""
}
export function setAdminToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token)
}
export function clearAdminToken(): void {
localStorage.removeItem(TOKEN_KEY)
}
export interface AdminQuestion {
id: string
format: QuizFormat
prompt: string
difficulty: number
source: string
choices: string[] | null
correctIndex: number | null
acceptedAnswers: string[] | null
category: string | null
}
export interface NewQuestionInput {
format: QuizFormat
prompt: string
category: string
difficulty: number
choices?: string[]
correctIndex?: number
acceptedAnswers?: string[]
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${SERVER_URL}/api/admin${path}`, {
...init,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getAdminToken()}`,
...init?.headers,
},
})
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string }
throw new Error(body.error ?? `Erreur ${res.status}`)
}
return res.status === 204 ? (undefined as T) : ((await res.json()) as T)
}
export const adminApi = {
listQuestions: () => request<AdminQuestion[]>("/questions"),
createQuestion: (input: NewQuestionInput) =>
request<{ id: string }>("/questions", {
method: "POST",
body: JSON.stringify(input),
}),
deleteQuestion: (id: string) =>
request<{ ok: true }>(`/questions/${id}`, { method: "DELETE" }),
}