Files
nebula/src/app/api/auth/setup/route.ts
T
Florian WildeandClaude Opus 5 36523a72d5 feat(auth): Einrichtung, Anmeldeseite und Registrierung
- /setup fuehrt beim ersten Aufruf durch das Anlegen des Administratorkontos
  und die Wahl der Betriebsart. Der Endpunkt funktioniert nur, solange es
  keinen Benutzer gibt; danach kann sich niemand mehr nachtraeglich ein
  Adminkonto erschleichen. Vorhandene Projekte ohne Besitzer werden dabei
  uebernommen und im Formular vorher angekuendigt.
- /login mit Passwort oder Passkey. Der Passkey-Knopf entfaellt, wenn APP_URL
  keinen sicheren Kontext ergibt - mit Hinweis, woran es liegt. Das Ziel nach
  der Anmeldung wird auf relative Pfade begrenzt, sonst waere der
  weiter-Parameter ein offener Redirect.
- /register nur im Mehrbenutzerbetrieb und nur, wenn ein Administrator die
  Selbstregistrierung freigegeben hat.
- Kopfzeile mit Benutzermenue: eigenes Konto, Admin-Einstellungen, Abmelden.
  Ohne Anmeldung bleibt sie auf das Logo reduziert.
- Layout liest Titel und Namen ueber generateMetadata, damit APP_NAME zur
  Laufzeit greift statt beim Build eingefroren zu werden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:30:33 +02:00

76 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextResponse } from "next/server";
import { hashPassword, validatePasswordStrength } from "@/lib/auth/password";
import { createSession, setSessionCookie } from "@/lib/auth/session";
import { claimOrphanedData } from "@/lib/auth/users";
import { prisma } from "@/lib/db";
import { clientIp, jsonError, readJsonBody } from "@/lib/security/request";
import { formatZodError, setupSchema } from "@/lib/validation";
export const dynamic = "force-dynamic";
/**
* Erstmalige Einrichtung: legt das Administratorkonto an.
*
* Funktioniert nur, solange es noch keinen Benutzer gibt – danach ist der
* Endpunkt tot. Damit kann sich niemand nachtraeglich ein Adminkonto
* erschleichen.
*/
export async function POST(request: Request) {
const existing = await prisma.user.count();
if (existing > 0) {
return jsonError("Die Einrichtung ist bereits abgeschlossen.", 409);
}
const body = await readJsonBody(request);
if (!body.ok) return body.response;
const parsed = setupSchema.safeParse(body.data);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validierung fehlgeschlagen", fields: formatZodError(parsed.error) },
{ status: 422 },
);
}
const { username, displayName, email, password, mode } = parsed.data;
const weak = validatePasswordStrength(password);
if (weak) {
return NextResponse.json(
{ error: weak, fields: { password: weak } },
{ status: 422 },
);
}
const user = await prisma.user.create({
data: {
username,
displayName: displayName ?? null,
email: email ?? null,
passwordHash: await hashPassword(password),
role: "ADMIN",
},
});
await prisma.appSettings.upsert({
where: { id: "singleton" },
create: { id: "singleton", mode },
update: { mode },
});
const claimed = await claimOrphanedData(user.id);
const session = await createSession(user.id, {
userAgent: request.headers.get("user-agent"),
ip: clientIp(request),
});
const response = NextResponse.json({
ok: true,
user: { id: user.id, username: user.username, role: user.role },
claimed,
});
setSessionCookie(response, session.token, session.expiresAt);
return response;
}