Files
alpenwerk-hr/lib/auth/require-hr.ts
Andrei Laas c6cff9656e
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m27s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m3s
Passwort-Anmeldung: Provider, Formulare, drei Zustaende der Shell
2026-09-08 16:51:14 +02:00

35 lines
1.6 KiB
TypeScript

import "server-only";
import { NextResponse } from "next/server";
import { sql, withUser } from "@/lib/db";
import { currentUserId } from "./session";
// 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 };
export async function requireHrUser(): Promise<HrGate> {
const userId = await currentUserId();
if (!userId) return { denied: NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }) };
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 (!erlaubt) return { denied: NextResponse.json({ error: "Nicht berechtigt." }, { status: 403 }) };
return { userId };
}