HTTP-Endpunkte und Anwendungsaufbau

Eingaben werden mit zod validiert, jede aendernde Aktion schreibt ins
Audit-Log. Beim ersten Start entsteht ein Administrator samt Startwerten fuer
die Produkterkennung; solange ein Erstpasswort gilt, bleiben schreibende Wege
bis auf den Passwortwechsel gesperrt.

Nicht-API-Pfade fallen auf die SPA zurueck, damit Deep-Links funktionieren.
This commit is contained in:
2026-09-18 11:37:31 +02:00
parent f3e31cdd17
commit a7228e63d1
10 changed files with 1209 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { requireAdmin } from '../auth/guard.js';
import { badRequest } from '../errors.js';
import {
applyMapping,
commitImport,
createImport,
discardImport,
getImport,
listImports,
parseUpload,
} from '../services/imports.js';
import { contextOf, idParam, parseWith } from './helpers.js';
const mappingBody = z.object({
playerKey: z.string().min(1, 'Spalte fuer die Spieler-Kennung fehlt'),
playerName: z.string().optional(),
product: z.string().min(1, 'Spalte fuer den Produktnamen fehlt'),
orderId: z.string().optional(),
purchasedAt: z.string().optional(),
createMissingPlayers: z.boolean().default(false),
});
export async function importRoutes(app: FastifyInstance): Promise<void> {
// Der Import veraendert Guthaben in grosser Zahl und bleibt deshalb Admins vorbehalten.
app.post('/api/imports', { preHandler: requireAdmin }, async (request, reply) => {
const file = await request.file();
if (!file) throw badRequest('NO_FILE', 'Es wurde keine Datei hochgeladen');
const content = await file.toBuffer();
const parsed = parseUpload(file.filename, content);
const importId = await createImport(file.filename, parsed, contextOf(request));
reply.code(201);
return {
importId,
columns: parsed.columns,
rowCount: parsed.rows.length,
sample: parsed.rows.slice(0, 5),
};
});
app.get('/api/imports', { preHandler: requireAdmin }, async () => {
return { items: await listImports() };
});
app.get('/api/imports/:id', { preHandler: requireAdmin }, async (request) => {
const { id } = parseWith(idParam, request.params);
return getImport(id);
});
app.post('/api/imports/:id/mapping', { preHandler: requireAdmin }, async (request) => {
const { id } = parseWith(idParam, request.params);
const body = parseWith(mappingBody, request.body);
const summary = await applyMapping(id, body, contextOf(request));
const record = await getImport(id);
return { summary, import: record };
});
app.post('/api/imports/:id/commit', { preHandler: requireAdmin }, async (request) => {
const { id } = parseWith(idParam, request.params);
return commitImport(id, contextOf(request));
});
app.post('/api/imports/:id/discard', { preHandler: requireAdmin }, async (request) => {
const { id } = parseWith(idParam, request.params);
await discardImport(id, contextOf(request));
return { ok: true };
});
}