Phase B1: Anmeldung, Benutzer, Gruppen und Rollen
Umschaltbar ueber AUTH_ENABLED in der .env. Steht sie auf false, verhaelt
sich alles wie bisher - geprueft mit den 200 bestehenden Pruefungen.
Kern ist eine Actor- und Policy-Schicht: getActor() liefert bei
ausgeschalteter Anmeldung einen system-Actor mit Vollzugriff, sonst den
angemeldeten Benutzer. Dadurch braucht keine einzige Route ein
'if (anmeldungAktiv)' - sie fragen alle nur can() bzw. requirePermission().
- Datenmodell: User, Session, Group, Membership; Codes bekommen Gruppe und
Ersteller. Die Migration legt eine Standardgruppe an und ordnet ihr
bestehende Codes zu, bevor die Spalte verpflichtend wird - sonst
scheiterte sie auf jeder bereits benutzten Installation.
- Passwoerter mit scrypt aus der Node-Standardbibliothek. Bewusst kein
Argon2: das braeuchte ein natives Modul passend zur Container-Plattform.
- Sitzungen in der Datenbank, im Cookie nur ein Zufallstoken; gespeichert
wird dessen SHA-256. Deaktivieren oder ein neues Passwort beendet alle
offenen Sitzungen sofort.
- Aussperrschutz: der letzte aktive Administrator kann sich nicht selbst
entmachten, deaktivieren oder loeschen. Der Schalter liegt in der .env,
nicht in der Oberflaeche - so bleibt der Weg zurueck immer offen.
- Ersteinrichtung legt den ersten Administrator an und schliesst sich danach.
- Die Weiterleitung /r/{slug} bleibt oeffentlich, sonst liesse sich kein
gedruckter Code mehr scannen.
Zwei Fehler dabei gefunden und behoben:
- 'return handleLogin(...)' innerhalb eines try-Blocks gibt das Promise
zurueck, bevor es sich aufloest. Die Ablehnung kam nie im catch an und
wurde zu einem nackten 500er statt einer 401. Jetzt mit await.
- Die Fehlerklassen wurden ueber instanceof erkannt. type-module.ts landet
aber sowohl im Server- als auch im Client-Buendel, es gibt also zwei
Klassenidentitaeten - saubere 400er wurden dadurch still zu 500ern. Die
Erkennung laeuft jetzt ueber eine Eigenschaft.
Geprueft: 51 neue Pruefungen fuer Anmeldung und die vollstaendige
Rollenmatrix, dazu die 200 bestehenden bei ausgeschalteter Anmeldung.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getActor } from "@/lib/auth/actor";
|
||||
import { resolveTargetGroup } from "@/lib/auth/groups";
|
||||
import { requirePermission, visibleGroupIds } from "@/lib/auth/policy";
|
||||
import { toJsonInput, toQrCodeDto } from "@/lib/codes";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
||||
@@ -21,6 +24,8 @@ const createSchema = z.object({
|
||||
.trim()
|
||||
.max(120, { error: "Der Name darf höchstens 120 Zeichen lang sein." })
|
||||
.optional(),
|
||||
/** In welcher Gruppe der Code angelegt wird. Ohne Anmeldung bedeutungslos. */
|
||||
groupId: z.string().trim().min(1).optional(),
|
||||
});
|
||||
|
||||
function isUniqueViolation(error: unknown): boolean {
|
||||
@@ -32,10 +37,24 @@ function isUniqueViolation(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Alle dynamischen Codes, neueste zuerst. */
|
||||
/** Die dynamischen Codes, die der Anfragende sehen darf — neueste zuerst. */
|
||||
export async function GET(): Promise<Response> {
|
||||
try {
|
||||
const codes = await prisma.qrCode.findMany({ orderBy: { createdAt: "desc" } });
|
||||
const actor = await getActor();
|
||||
if (actor.kind === "anonymous") {
|
||||
throw new QrRequestError("Bitte melde dich an.", 401);
|
||||
}
|
||||
|
||||
const visible = visibleGroupIds(actor);
|
||||
const codes = await prisma.qrCode.findMany({
|
||||
where: visible === null ? undefined : { groupId: { in: visible } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: {
|
||||
group: { select: { name: true } },
|
||||
createdBy: { select: { name: true, email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json({ codes: codes.map(toQrCodeDto) });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
@@ -45,12 +64,19 @@ export async function GET(): Promise<Response> {
|
||||
/** Legt einen dynamischen Code an und vergibt dafür eine freie Kurz-URL. */
|
||||
export async function POST(request: NextRequest): Promise<Response> {
|
||||
try {
|
||||
const actor = await getActor();
|
||||
|
||||
const parsed = createSchema.safeParse(await request.json());
|
||||
if (!parsed.success) {
|
||||
return jsonError(firstZodMessage(parsed.error));
|
||||
}
|
||||
|
||||
const { type, design, label } = parsed.data;
|
||||
|
||||
// Zielgruppe zuerst: davon hängt ab, ob überhaupt angelegt werden darf.
|
||||
const groupId = await resolveTargetGroup(actor, parsed.data.groupId);
|
||||
requirePermission(actor, "codes:create", { groupId });
|
||||
|
||||
const qrType = await requireAvailableQrType(type);
|
||||
|
||||
const validated = qrType.validate(parsed.data.data);
|
||||
@@ -70,8 +96,14 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
type: qrType.id,
|
||||
label: label && label.length > 0 ? label : null,
|
||||
data: toJsonInput(validated.data),
|
||||
groupId,
|
||||
createdById: actor.kind === "user" ? actor.user.id : null,
|
||||
...design,
|
||||
},
|
||||
include: {
|
||||
group: { select: { name: true } },
|
||||
createdBy: { select: { name: true, email: true } },
|
||||
},
|
||||
});
|
||||
return Response.json({ code: toQrCodeDto(created) }, { status: 201 });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user