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>
65 lines
1.8 KiB
TypeScript
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" }),
|
|
}
|