feat: Grundgerüst mit Anmeldung, Layout und Design-Tokens
Next.js 16, Prisma 7, Tailwind 4 und Vitest/Playwright, Konfiguration aus Laufzettel übernommen, weil dort derselbe Stack erprobt ist. Die Farben der Branding-Vorgabe stehen als Tokens in globals.css, das helle Theme setzt dieselben Namen neu. Amber und Rot sind dort abgedunkelt, sonst reicht der Kontrast nicht – ein Test prüft beide Themes gegen 4,5:1. Anmeldung mit lokalem Benutzer, Rolle je Anfrage aus der Datenbank, Audit-Log per Trigger nur anhängbar. Geprüft im gebauten Stand auf 1440 px und 390 px. Closes #001 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { aufruferAdresse } from "@/lib/adressen";
|
||||
import { SITZUNG_COOKIE } from "@/lib/sitzung";
|
||||
import { api } from "@/server/api";
|
||||
import { anfrageAufrufer } from "@/server/auth/zugang";
|
||||
import { protokollieren } from "@/server/services/protokoll";
|
||||
|
||||
/** Öffentlich: Abmelden muss auch mit abgelaufener Sitzung gehen. */
|
||||
export const POST = api(async (anfrage: Request) => {
|
||||
const aufrufer = await anfrageAufrufer(anfrage);
|
||||
if (aufrufer) {
|
||||
await protokollieren({
|
||||
benutzerId: aufrufer.benutzer.id,
|
||||
quelle: aufrufer.quelle,
|
||||
aktion: "Abgemeldet",
|
||||
ip: aufruferAdresse(anfrage.headers),
|
||||
});
|
||||
}
|
||||
const antwort = NextResponse.json({ ok: true });
|
||||
antwort.cookies.delete(SITZUNG_COOKIE);
|
||||
return antwort;
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { aufruferAdresse, istHttps } from "@/lib/adressen";
|
||||
import { SITZUNG_COOKIE, SITZUNG_LAUFZEIT_STUNDEN, sitzungErzeugen } from "@/lib/sitzung";
|
||||
import { z } from "@/lib/validierung";
|
||||
import { api, jsonLesen } from "@/server/api";
|
||||
import { mitPasswortAnmelden } from "@/server/services/anmeldung";
|
||||
|
||||
const Anmeldung = z.object({
|
||||
benutzername: z.string().trim().min(1, "Benutzername fehlt.").max(64),
|
||||
passwort: z.string().min(1, "Passwort fehlt.").max(1024),
|
||||
});
|
||||
|
||||
/** Öffentlich (Proxy-Ausnahme) – sonst könnte sich niemand anmelden. */
|
||||
export const POST = api(async (anfrage: Request) => {
|
||||
const eingabe = Anmeldung.parse(await jsonLesen(anfrage));
|
||||
const benutzer = await mitPasswortAnmelden(
|
||||
eingabe.benutzername,
|
||||
eingabe.passwort,
|
||||
aufruferAdresse(anfrage.headers),
|
||||
);
|
||||
|
||||
const antwort = NextResponse.json({ benutzername: benutzer.benutzername, rolle: benutzer.rolle });
|
||||
antwort.cookies.set(
|
||||
SITZUNG_COOKIE,
|
||||
await sitzungErzeugen({ benutzerId: benutzer.id, herkunft: "passwort" }),
|
||||
{
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: istHttps(anfrage.headers, anfrage.url),
|
||||
path: "/",
|
||||
maxAge: SITZUNG_LAUFZEIT_STUNDEN * 3600,
|
||||
},
|
||||
);
|
||||
return antwort;
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { VERSION } from "@/lib/version";
|
||||
|
||||
/** Healthcheck für Docker und Reverse-Proxy. Öffentlich, verrät nur die Version. */
|
||||
export async function GET(): Promise<Response> {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
return NextResponse.json({ status: "ok", version: VERSION });
|
||||
} catch {
|
||||
return NextResponse.json({ status: "datenbank_nicht_erreichbar", version: VERSION }, { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { VERSION } from "@/lib/version";
|
||||
import { api } from "@/server/api";
|
||||
import { RECHTE, darf, type Recht } from "@/server/auth/rechte";
|
||||
import { rechtErforderlich } from "@/server/auth/zugang";
|
||||
|
||||
/** Wer bin ich, was darf ich – für die CLI (`proxdeploy login`) und die GUI. */
|
||||
export const GET = api(async (anfrage: Request) => {
|
||||
const { benutzer } = await rechtErforderlich(anfrage, "lesen");
|
||||
const rechte = (Object.keys(RECHTE) as Recht[]).filter((recht) => darf(benutzer.rolle, recht));
|
||||
return NextResponse.json({
|
||||
benutzername: benutzer.benutzername,
|
||||
anzeigename: benutzer.anzeigename,
|
||||
rolle: benutzer.rolle,
|
||||
rechte,
|
||||
version: VERSION,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user