# Manner HR Master Interne HR-Stammdatenverwaltung: Mitarbeiter:innen, Organisationsstruktur (Bereich/Abteilung/Team), Planstellen, Neueinstellungen, Versetzungen/ Beförderungen/Karenz, Reorganisationen und der zugehörige Audit-Trail. ## Zweck Die App ersetzt Excel-basierte HR-Stammdatenpflege durch ein Werkzeug mit verbindlichen Regeln (z. B. wirksame Daten statt sofortiger Änderungen, eindeutige Positions-/Org-Nummern, verpflichtende Historie) und einem lückenlosen Audit-Trail für jede Änderung. **Alle Mitarbeiterdaten in diesem System sind vertraulich** — Stammdaten, Verträge, Sozialversicherungsnummern, Angehörige und Audit-Daten. Zugriff ist auf explizit aktivierte HR-Benutzer:innen beschränkt (siehe [Sicherheitsprinzipien](#sicherheitsprinzipien)). ## Tech-Stack - Next.js 16 (App Router) — **Achtung:** Next.js 16 hat Breaking Changes gegenüber älteren Versionen (u. a. `proxy.ts` statt `middleware.ts`). Vor Änderungen an Framework-nahen Dateien die lokalen Docs unter `node_modules/next/dist/docs/` konsultieren; sie sind maßgeblich, ältere Anleitungen im Netz beschreiben teils überholte APIs. - React 19, TypeScript - Supabase (Postgres, Auth, RLS) — Datenhaltung liegt vollständig in Supabase, nicht im Next.js-Prozess. - Tailwind CSS v4 - Vitest — Unit- (Node), Komponenten- (jsdom) und Integrationstests (gegen ein lokales Supabase) ## Setup ```bash npm install cp .env.example .env.local # Werte eintragen, siehe unten npm run dev ``` Für lokale Supabase-Entwicklung (statt gegen ein Cloud-Projekt): ```bash supabase start # startet lokalen Postgres/Auth/Studio-Stack ``` `supabase/config.toml` und `.env.test.local` sind bereits auf die Standard-Ports der lokalen Supabase-CLI abgestimmt. ## Umgebungsvariablen Siehe [`.env.example`](.env.example) für die vollständige, kommentierte Liste. Kurzfassung: | Variable | Sichtbarkeit | Zweck | |---|---|---| | `DATABASE_URL` | Nur Server | PostgreSQL-Verbindung. Die Rolle darf **kein** `BYPASSRLS` haben | | `DATABASE_SSL` | Nur Server | `false` für lokal/CI ohne TLS | | `AUTH_SECRET` | Nur Server | Signiert und verschlüsselt das Sitzungscookie | | `AUTH_MICROSOFT_ENTRA_ID_ID` | Nur Server | Anwendungs-ID der Entra-Registrierung | | `AUTH_MICROSOFT_ENTRA_ID_SECRET` | Nur Server | Client-Geheimnis dazu | | `AUTH_MICROSOFT_ENTRA_ID_ISSUER` | Nur Server | Aussteller mit Mandanten-ID — nicht `common` | | `CRON_SECRET` | Nur Server | Schützt `/api/cron/apply-pending-changes` | **Es gibt keine `NEXT_PUBLIC_*`-Variablen mehr.** Nichts wird in das Browser-Bundle eingebacken, weil der Browser mit nichts ausser der Anwendung selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut, überall dasselbe — vorher brauchte jede Umgebung ihr eigenes. ## Scripts | Befehl | Zweck | |---|---| | `npm run dev` | Lokaler Dev-Server | | `npm run build` | Produktions-Build | | `npm run start` | Produktions-Server (nach `build`) | | `npm run lint` | ESLint (`eslint-config-next`, Flat Config) | | `npm run typecheck` | `tsc --noEmit` | | `npm run test` | Vitest, Unit-Tests (`tests/unit/**`) | | `npm run test:integration` | Vitest gegen eine echte (lokale) Supabase-Instanz — braucht `supabase start` und `.env.test.local` | | `npm run test:e2e` | Playwright | | `npm run check` | lint + typecheck + test + build in Folge | ## Sicherheitsprinzipien - **RLS ist die eigentliche Schranke, nicht die UI.** Jede Tabelle hat Row Level Security aktiv; `proxy.ts` (App-Ebene) ist Defense-in-Depth, keine Ersatzkontrolle. - **Ein Rollenmodell:** `profiles.role = 'hr'` + `profiles.is_active = true`, geprüft über die SQL-Funktion `is_hr_user()`. Kein Sub-Rollensystem — siehe [`docs/data-model.md`](docs/data-model.md#zugriffsmodell). - **Es gibt keinen privilegierten Zugang mehr.** Der Dienstschlüssel, der RLS aushebelte, ist ersatzlos entfallen; auch der nächtliche Lauf benutzt dieselbe Rolle ohne `BYPASSRLS`. Was ohne angemeldete Person laufen muss, steht als `SECURITY DEFINER`-Funktion in der Datenbank und prüft dort selbst, was es tut. - **Jede Abfrage läuft in einer Transaktion mit gesetztem Sitzungskontext.** Die Kysely-Instanz wird nicht exportiert — der einzige Weg an die Datenbank ist `withUser()` (`lib/db/index.ts`), und eine ESLint-Regel verbietet den Import von `pg` ausserhalb von `lib/db/`. - **Audit-Log ist transaktional in der Datenbank**, nicht im App-Code: jede mutierende SQL-Funktion schreibt ihren `audit_log`-Eintrag in derselben Transaktion wie die Änderung selbst. Details und Prüfung siehe [`docs/security-review.md`](docs/security-review.md). - **Historie ist append-only** (`employee_history`, `audit_log`) — RLS erlaubt kein `update`/`delete`. Korrekturen sind kompensierende Einträge. ## Cron-Konfiguration `/api/cron/apply-pending-changes` wendet wirksam gewordene, zukunftsdatierte Änderungen an (`pending_org_changes` → `apply_due_pending_changes()`). - **Auf Vercel:** `vercel.json` definiert den täglichen Schedule; Vercel Cron sendet `Authorization: Bearer ` automatisch, wenn `CRON_SECRET` in den Projekt-Env-Vars gesetzt ist. - **Außerhalb von Vercel (Docker):** kein Vercel Cron verfügbar — siehe [`DEPLOYMENT.md`](DEPLOYMENT.md) für den Cron-Sidecar-Container, der denselben Endpoint mit demselben Schema aufruft. - Fehlt `CRON_SECRET` oder stimmt der Header nicht, antwortet die Route mit `401` (nicht `500` — bewusst, siehe `tests/unit/security.test.ts`). ## Supabase-Hinweise - Schema-Quelle der Wahrheit: `supabase/migrations/`. Menschlich lesbare Zusammenfassung: [`docs/data-model.md`](docs/data-model.md). - Migrationen einspielen: `supabase db push` (gegen das verlinkte Projekt) bzw. `supabase start` + automatische Anwendung für lokale Entwicklung. - `supabase/seed.ts` und `.env.test.local` sind nur für lokale Entwicklung/Tests gedacht, nie für ein Produktivprojekt verwenden. ## Testing - `npm run test` — schnell, keine externen Abhängigkeiten, läuft in CI. - `npm run test:integration` — braucht eine laufende lokale Supabase-Instanz (`supabase start`) und `.env.test.local`; prüft RLS-Verhalten end-to-end (siehe `tests/integration/authorization.test.ts` für das HR-Only-Zugriffs- modell). - `npm run test:e2e` — Playwright gegen einen laufenden Dev-/Preview-Server. ## Deployment Siehe [`DEPLOYMENT.md`](DEPLOYMENT.md) für Docker-basiertes Deployment (Dockerfile, docker-compose.yml, Reverse-Proxy/TLS, Cron-Ersatz, Updates). Für Vercel: `vercel.json` ist bereits vorhanden; Env-Vars im Vercel-Projekt setzen (siehe oben). ## Known TODOs vor Produktivbetrieb - **Content-Security-Policy fehlt noch** (`next.config.ts` setzt bewusst keine CSP — Skript-/Style-/Connect-Quellen sind noch nicht vollständig inventarisiert; ungeprüft geraten zu setzen riskiert, Hydration oder den Supabase-Client stillschweigend zu brechen). - **Lokale Scratch-Artefakte** (`.scratch_*`, `.scratch_shots/`) enthalten Screenshots/Hilfsskripte aus einer früheren manuellen Verifikation und liegen noch im Arbeitsverzeichnis. Sie sind jetzt über `.gitignore` ausgeschlossen; vor einem Produktiv-Handover sollten sie durchgesehen und bei Bedarf gelöscht werden. - **Kein granulareres Rollenmodell** — aktuell HR-only (alles-oder-nichts). Falls z. B. eine reine Lese-Rolle künftig gebraucht wird, gehört die Erweiterung in eine neue Migration (`is_hr_user()`/RLS-Policies), nicht in App-seitigen Code.