Passwort-Anmeldung: Provider, Formulare, drei Zustaende der Shell
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m27s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m3s

This commit is contained in:
2026-09-08 16:51:14 +02:00
parent cbde8cf3a8
commit c6cff9656e
13 changed files with 569 additions and 40 deletions

View File

@@ -1,13 +1,22 @@
import "server-only";
import { NextResponse } from "next/server";
import { withUser } from "@/lib/db";
import { sql, withUser } from "@/lib/db";
import { currentUserId } from "./session";
// Route Handlers under /api/export/* are outside the App Router layout tree,
// so app/(app)/layout.tsx's HR gate never runs for them — each one has to
// re-establish that the caller is an active HR user itself. RLS is still the
// real boundary (an unauthorized session simply reads nothing); this exists
// so those routes answer 401/403 instead of handing back an empty workbook.
// Route Handlers under /api/export/* and /api/import are outside the App
// Router layout tree, so app/(app)/layout.tsx's HR gate never runs for them —
// each one has to re-establish that the caller is an active HR user itself.
// RLS is still the real boundary (an unauthorized session simply reads
// nothing); this exists so those routes answer 401/403 instead of handing back
// an empty workbook.
//
// Gefragt wird is_hr_user() und nicht mehr profiles.role/is_active von Hand.
// Der Unterschied ist nicht kosmetisch: is_hr_user() ist dieselbe Funktion, die
// alle 25 RLS-Policies aufrufen. Was immer sie künftig zusätzlich prüft, gilt
// hier automatisch mit — beim ausstehenden Passwortwechsel ist genau das schon
// passiert (Migration 20260908120000). Die Handfassung hätte davon nichts
// gewusst und einen Export ausgeliefert, den die Policies darunter leer
// gelassen hätten.
export type HrGate = { denied: NextResponse } | { userId: string };
@@ -15,12 +24,11 @@ export async function requireHrUser(): Promise<HrGate> {
const userId = await currentUserId();
if (!userId) return { denied: NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }) };
const profile = await withUser(userId, (tx) =>
tx.selectFrom("profiles").select(["role", "is_active"]).where("id", "=", userId).executeTakeFirst()
);
const erlaubt = await withUser(userId, async (tx) => {
const ergebnis = await sql<{ ok: boolean }>`select is_hr_user() as ok`.execute(tx);
return ergebnis.rows[0]?.ok === true;
});
if (profile?.role !== "hr" || profile.is_active !== true) {
return { denied: NextResponse.json({ error: "Nicht berechtigt." }, { status: 403 }) };
}
if (!erlaubt) return { denied: NextResponse.json({ error: "Nicht berechtigt." }, { status: 403 }) };
return { userId };
}