The one document describing the schema, docs/data-model.md, predates two rebuilds. It names divisions/departments/teams and a positions table that no longer exist, describes Supabase auth with an anon key and a service role that were removed, and puts the policy count at 58 when it is 21. Anyone reading it to understand the data would have been misled on every count. docs/datenkatalog.md replaces it, and was not typed up from memory: the columns, defaults, keys and check constraints were read out of information_schema and pg_catalog on the running database. Fifteen tables, 142 columns, ten enum types, 21 policies. Where a rule appears in prose, the constraint it comes from is named next to it. Some of it only became visible by asking the database rather than the migrations. generate_company_email and the is_hr_admin pair are still defined but nothing calls them any more. Position numbers look like a six followed by seven digits because the generator builds them that way, not because anything enforces it — the column requires only uniqueness. monthly_salary_gross is dead weight kept in case old rows hold data. Three claims I drafted were wrong and the database said so: the position number format, the event trigger's name (ensure_rls, the function behind it is rls_auto_enable), and which tables deviate from the plain is_hr_user() policy. The old document keeps a pointer at the top instead of being deleted — it is linked from the security review, and a stale document that says so is more useful than a dead link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
163 lines
7.4 KiB
Markdown
163 lines
7.4 KiB
Markdown
# 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/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()`).
|
|
|
|
- **Auf Vercel:** `vercel.json` definiert den täglichen Schedule; Vercel Cron
|
|
sendet `Authorization: Bearer <CRON_SECRET>` 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
|
|
Fassung, aus der laufenden Datenbank erzeugt:
|
|
[`docs/datenkatalog.md`](docs/datenkatalog.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.
|