# Alpenwerk 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 - PostgreSQL, angesprochen über Kysely und `pg` — die Datenhaltung liegt in der Datenbank, nicht im Next.js-Prozess. Der Zugriff läuft ausschliesslich über `withUser()` (`lib/db/`), das den Sitzungskontext setzt. - Auth.js gegen Microsoft Entra ID — keine eigenen Passwörter. - Tailwind CSS v4 - Vitest — Unit- (Node), Komponenten- (jsdom) und Integrationstests (gegen eine erreichbare Datenbank) ## Setup ```bash npm install cp .env.example .env.local # Werte eintragen, siehe unten npm run dev ``` Für eine lokale Datenbank genügt der Container aus `docker-compose.yml`: ```bash docker compose up -d db docker compose run --rm migrate # spielt db/migrations/ ein ``` Danach zeigt `DATABASE_URL` in `.env.local` auf diese Datenbank. ## 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 erreichbare Datenbank; überspringt sich ohne `DATABASE_URL` | | `npm run test:e2e` | Playwright | | `npm run migrate` | Ausstehende Migrationen einspielen (`--dry-run`, `--baseline`) | | `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/datenkatalog.md`](docs/datenkatalog.md#zugriffsschutz). - **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()`). - Den Zeitplan hält der `cron`-Container aus `docker-compose.yml`: täglich 03:00 Uhr, mit `Authorization: Bearer `. - Fehlt `CRON_SECRET` oder stimmt der Header nicht, antwortet die Route mit `401` (nicht `500` — bewusst, siehe `tests/unit/security.test.ts`). ## Datenbank - Schema-Quelle der Wahrheit: `db/migrations/`. Menschlich lesbare Fassung, aus der laufenden Datenbank erzeugt: [`docs/datenkatalog.md`](docs/datenkatalog.md). - Migrationen einspielen: `npm run migrate`. Der Läufer wendet nur die fehlenden Dateien an, jede in ihrer eigenen Transaktion, und führt darüber Buch in `migrationen.schema_migrations`. - `lib/types.ts` wird von Hand gepflegt. `npm run types:check` hält sie Spalte für Spalte gegen die Migrationen. ## Testing - `npm run test` — schnell, keine externen Abhängigkeiten, läuft in CI. - `npm run test:integration` — braucht eine erreichbare Datenbank (`DATABASE_URL`). Geprüft werden Regeln, die es zweimal gibt: einmal als SQL, einmal als TypeScript. Ohne `DATABASE_URL` überspringen sich die Dateien, statt mit einem Verbindungsfehler abzubrechen. - `npm run test:e2e` — Playwright gegen einen laufenden Dev-/Preview-Server. ## Deployment Siehe [`DEPLOYMENT.md`](DEPLOYMENT.md): Anwendung und Datenbank als Container, Reverse-Proxy/TLS, der nächtliche Lauf, Migrationen und Updates. ## Known TODOs vor Produktivbetrieb - **Content-Security-Policy läuft im Nur-Bericht-Modus** (`next.config.ts`). Erzwungen wird sie erst, wenn die Meldungen sauber sind — eine geratene, erzwungene Richtlinie blendet die Anwendung für alle aus. - **Seed für die Integrationstests fehlt.** Er lag in der abgelösten Umgebung. Zwei der drei Integrationstests vergleichen Regeln über den gesamten Bestand und brauchen dafür Daten; bis der Seed nachgezogen ist, laufen sie nur gegen eine bereits befüllte Datenbank, nicht in der CI. - **Sicherheitsprüfung des heutigen Aufbaus steht aus** — siehe [`docs/security-review.md`](docs/security-review.md). - **Bildschirmfotos unter `.scratch_shots/`** stammen aus einer früheren Handprüfung. Sie sind über `.gitignore` ausgeschlossen; vor der Übergabe durchsehen und löschen. - **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.