Consolidation pass: HR-only access, effective-dated mutations, data integrity guards, test suite
Reworks the app from a two-role (hr_admin/manager) model to a single HR-only role gated by profiles.is_active, fixes transfer/promote/karenz/ reorg RPCs to actually defer future-dated changes via a new pending_org_changes table instead of writing them immediately (applied by a daily Vercel Cron route), makes reorg undo append-only instead of deleting history, adds Karenz-return and history-date integrity guards, deprecates the salary column, and adds explicit schema grants + perf indexes needed to run against a fresh (non-hosted) Postgres instance. Adds vitest unit + integration test suites (the latter against a real local Supabase instance) covering all of the above, plus lint/typecheck/ build wiring (`npm run check`).
This commit is contained in:
29
proxy.ts
29
proxy.ts
@@ -2,9 +2,16 @@ import { createServerClient } from "@supabase/ssr";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
// Next.js 16 renamed Middleware to Proxy (same mechanism, new filename/export).
|
||||
// This performs the optimistic auth check: redirect unauthenticated users to
|
||||
// /login, and signed-in users away from /login. Real authorization (hr_admin
|
||||
// vs manager) is enforced server-side via RLS, not here.
|
||||
// This is the app's single entry-point gate (spec
|
||||
// §2.2): unauthenticated users are sent to /login, and — this is the part
|
||||
// that used to be missing — authenticated users who are NOT an active,
|
||||
// explicitly-provisioned HR user are sent to /login too, with an error
|
||||
// message, instead of being let through. Previously this only checked for
|
||||
// a Supabase Auth session at all, which meant any signed-in user (even one
|
||||
// with no profile row, or the old "manager" role) could open the app.
|
||||
// This UI-layer gate is defense in depth, not the real boundary — every
|
||||
// table is independently RLS-gated on is_hr_user() regardless of what this
|
||||
// proxy does.
|
||||
export async function proxy(request: NextRequest) {
|
||||
let response = NextResponse.next({ request });
|
||||
|
||||
@@ -31,13 +38,25 @@ export async function proxy(request: NextRequest) {
|
||||
|
||||
const isLoginRoute = request.nextUrl.pathname.startsWith("/login");
|
||||
|
||||
if (!user && !isLoginRoute) {
|
||||
if (!user) {
|
||||
if (isLoginRoute) return response;
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
if (user && isLoginRoute) {
|
||||
const { data: profile } = await supabase.from("profiles").select("role, is_active").eq("id", user.id).maybeSingle();
|
||||
const isActiveHr = profile?.role === "hr" && profile?.is_active === true;
|
||||
|
||||
if (!isActiveHr) {
|
||||
if (isLoginRoute) return response;
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/login";
|
||||
url.searchParams.set("error", "Kein HR-Zugriff. Bitte wenden Sie sich an eine:n bestehende:n HR-Benutzer:in.");
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
if (isLoginRoute) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/";
|
||||
return NextResponse.redirect(url);
|
||||
|
||||
Reference in New Issue
Block a user