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:
+50
-20
@@ -1,7 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { LogoutButton } from "@/components/auth-forms";
|
||||
import { Icon } from "@/components/icons";
|
||||
import { actorLabel, getActor, isAuthEnabled } from "@/lib/auth/actor";
|
||||
import { can } from "@/lib/auth/policy";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -13,7 +16,18 @@ export const metadata: Metadata = {
|
||||
"QR-Codes erstellen: statisch oder dynamisch, mit eigenen Farben, frei wählbarer Größe und Download als PNG oder SVG.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
// Der Kopfbereich hängt von der Anmeldung ab und darf nicht vorgerendert
|
||||
// werden.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const NAV_LINK =
|
||||
"rounded-lg px-3 py-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900";
|
||||
|
||||
export default async function RootLayout({ children }: LayoutProps<"/">) {
|
||||
const actor = await getActor();
|
||||
const signedIn = actor.kind !== "anonymous";
|
||||
const showAdmin = can(actor, "instance:manage");
|
||||
|
||||
return (
|
||||
<html lang="de" className="h-full">
|
||||
<body className="flex min-h-full flex-col">
|
||||
@@ -30,25 +44,41 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
Navigation nicht mehr in eine Zeile. */}
|
||||
<span className="hidden sm:inline">QR-Code-Generator</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-0.5 text-sm font-medium sm:gap-1">
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-lg px-3 py-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
Erstellen
|
||||
</Link>
|
||||
<Link
|
||||
href="/meine-codes"
|
||||
className="rounded-lg px-3 py-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
Meine Codes
|
||||
</Link>
|
||||
<Link
|
||||
href="/verwaltung/typen"
|
||||
className="rounded-lg px-3 py-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
Verwaltung
|
||||
</Link>
|
||||
|
||||
<nav className="flex flex-wrap items-center gap-0.5 text-sm font-medium sm:gap-1">
|
||||
{signedIn ? (
|
||||
<>
|
||||
<Link href="/" className={NAV_LINK}>
|
||||
Erstellen
|
||||
</Link>
|
||||
<Link href="/meine-codes" className={NAV_LINK}>
|
||||
Meine Codes
|
||||
</Link>
|
||||
{showAdmin ? (
|
||||
<Link href="/verwaltung" className={NAV_LINK}>
|
||||
Verwaltung
|
||||
</Link>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isAuthEnabled() && actor.kind === "user" ? (
|
||||
<>
|
||||
<span
|
||||
className="ml-1 hidden max-w-[14rem] truncate text-slate-500 sm:inline"
|
||||
title={actor.user.email}
|
||||
>
|
||||
{actorLabel(actor)}
|
||||
</span>
|
||||
<LogoutButton />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isAuthEnabled() && actor.kind === "anonymous" ? (
|
||||
<Link href="/anmelden" className={NAV_LINK}>
|
||||
Anmelden
|
||||
</Link>
|
||||
) : null}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user