35 lines
1.6 KiB
TypeScript
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 };
|
|
}
|