Remove Supabase
The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,3 @@ npm-debug.log*
|
||||
*.tsbuildinfo
|
||||
.scratch_*
|
||||
.scratch_shots
|
||||
supabase/.branches
|
||||
supabase/.temp
|
||||
supabase/snippets
|
||||
|
||||
101
.github/workflows/ci.yml
vendored
101
.github/workflows/ci.yml
vendored
@@ -29,25 +29,45 @@ jobs:
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
# Catches the drift that `tsc` cannot: lib/supabase/types.ts is
|
||||
# hand-written, so a migration adding a column leaves it silently stale.
|
||||
# Fängt die Abweichung, die `tsc` nicht sehen kann: lib/types.ts wird von
|
||||
# Hand gepflegt, eine Migration mit einer neuen Spalte lässt sie also
|
||||
# stillschweigend veralten.
|
||||
- name: Schema/Typen-Abgleich
|
||||
run: npm run types:check
|
||||
|
||||
- name: Unit- und Komponententests
|
||||
run: npm test
|
||||
|
||||
# Ohne Umgebungsvariablen: seit dem Wegfall der Browser-Anbindung wird
|
||||
# zur Bauzeit nichts mehr aus der Umgebung gelesen, und das Abbild ist
|
||||
# für jede Umgebung dasselbe.
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
# Read at module scope by the Supabase browser client, so the build
|
||||
# needs them present — never the real project's values.
|
||||
NEXT_PUBLIC_SUPABASE_URL: http://127.0.0.1:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: build-time-placeholder
|
||||
|
||||
integration:
|
||||
name: Integrationstests (echtes Postgres)
|
||||
migrationen:
|
||||
name: Migrationen auf leerer Datenbank
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Derselbe Stand wie im Betrieb. Ein eigener Dienst statt einer fremden
|
||||
# Plattform — die Datenbank gehört seit dem Umzug zum Projekt.
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_PASSWORD: ci
|
||||
POSTGRES_DB: alpenwerk
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U postgres"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 20
|
||||
|
||||
env:
|
||||
MIGRATE_DATABASE_URL: postgresql://postgres:ci@127.0.0.1:5432/alpenwerk
|
||||
DATABASE_SSL: "false"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -58,40 +78,37 @@ jobs:
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- uses: supabase/setup-cli@v1
|
||||
with:
|
||||
version: latest
|
||||
|
||||
# Applies every migration to a fresh database — which also means a
|
||||
# migration that cannot be replayed from scratch fails here rather than
|
||||
# on a restore or a new environment.
|
||||
- name: Supabase starten
|
||||
run: supabase start
|
||||
|
||||
# `-o env` emits API_URL / ANON_KEY / SERVICE_ROLE_KEY / DB_URL; the app
|
||||
# expects them under its own names. DATABASE_URL ist der direkte
|
||||
# Postgres-Zugang — den braucht die neue Zugriffsschicht (lib/db) und
|
||||
# vor allem der Nachweis zum Sitzungskontext.
|
||||
- name: Testumgebung schreiben
|
||||
# Was das Schema von der Umgebung erwartet: die Anwendungsrolle ohne
|
||||
# BYPASSRLS, zwei Erweiterungen und die Attrappe des `auth`-Schemas, ohne
|
||||
# die sich die Migrationen von Juni 2026 nicht abspielen lassen.
|
||||
- name: Rollen, Erweiterungen, auth-Attrappe
|
||||
env:
|
||||
PGPASSWORD: ci
|
||||
APP_DB_PASSWORD: ci-anwendungsrolle
|
||||
run: |
|
||||
supabase status -o env \
|
||||
--override-name api.url=NEXT_PUBLIC_SUPABASE_URL \
|
||||
--override-name auth.anon_key=NEXT_PUBLIC_SUPABASE_ANON_KEY \
|
||||
--override-name auth.service_role_key=SUPABASE_SERVICE_ROLE_KEY \
|
||||
--override-name db.url=DATABASE_URL \
|
||||
| grep -E '^(NEXT_PUBLIC_SUPABASE_URL|NEXT_PUBLIC_SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL)=' \
|
||||
| tr -d '"' > .env.test.local
|
||||
# Lokal läuft Postgres ohne TLS; ohne das versucht `pg` es trotzdem.
|
||||
echo "DATABASE_SSL=false" >> .env.test.local
|
||||
grep -q '^DATABASE_URL=' .env.test.local \
|
||||
|| { echo "DATABASE_URL wurde nicht geschrieben — der Sitzungskontext-Nachweis liefe ins Leere."; exit 1; }
|
||||
for f in deploy/db-init/*.sql; do
|
||||
echo "── $f"
|
||||
psql -v ON_ERROR_STOP=1 -h 127.0.0.1 -U postgres -d alpenwerk -f "$f"
|
||||
done
|
||||
|
||||
- name: Seed
|
||||
run: node --env-file=.env.test.local supabase/seed.ts
|
||||
# Der eigentliche Zweck dieses Jobs: jede Migration muss sich auf einer
|
||||
# leeren Datenbank abspielen lassen. Eine, die das nicht kann, fällt hier
|
||||
# auf — und nicht beim Wiederherstellen einer Sicherung oder beim
|
||||
# Aufsetzen einer neuen Umgebung.
|
||||
- name: Migrationen einspielen
|
||||
run: node scripts/migrate.mjs
|
||||
|
||||
- name: Integrationstests
|
||||
run: npm run test:integration
|
||||
# Zweiter Lauf: die Buchführung muss greifen. Meldet er etwas anderes als
|
||||
# „nichts anzuwenden", verbucht der Läufer nicht richtig — und ein
|
||||
# Ausrollen spielte Migrationen doppelt ein.
|
||||
- name: Zweiter Lauf ist ein Nichts
|
||||
run: |
|
||||
ausgabe=$(node scripts/migrate.mjs)
|
||||
echo "$ausgabe"
|
||||
echo "$ausgabe" | grep -q "^Nichts anzuwenden" \
|
||||
|| { echo "Der zweite Lauf wollte erneut anwenden — die Buchführung greift nicht."; exit 1; }
|
||||
|
||||
- name: Supabase-Logs bei Fehlschlag
|
||||
if: failure()
|
||||
run: supabase status && docker ps -a
|
||||
# Die Integrationstests brauchen einen befüllten Bestand; der Seed lag
|
||||
# in der abgelösten Umgebung und ist noch nicht nachgezogen (siehe
|
||||
# README, offene Punkte). Bis dahin laufen sie gegen eine erreichbare
|
||||
# Datenbank von Hand, nicht hier.
|
||||
|
||||
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
||||
- name: .env schreiben
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
|
||||
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
||||
AUTH_URL: ${{ secrets.AUTH_URL }}
|
||||
AUTH_MICROSOFT_ENTRA_ID_ID: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_ID }}
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fehlend=""
|
||||
for name in DATABASE_URL SUPABASE_DB_URL AUTH_SECRET AUTH_URL \
|
||||
for name in DATABASE_URL MIGRATE_DATABASE_URL AUTH_SECRET AUTH_URL \
|
||||
AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET \
|
||||
AUTH_MICROSOFT_ENTRA_ID_ISSUER CRON_SECRET; do
|
||||
eval "wert=\${$name:-}"
|
||||
@@ -138,12 +138,12 @@ jobs:
|
||||
# wenn danach etwas schiefgeht.
|
||||
- name: Ausstehende Migrationen zeigen
|
||||
env:
|
||||
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
|
||||
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||
run: node scripts/migrate.mjs --dry-run
|
||||
|
||||
- name: Migrationen anwenden
|
||||
env:
|
||||
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
|
||||
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||
run: node scripts/migrate.mjs
|
||||
|
||||
# ── Container ───────────────────────────────────────────────────
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -38,17 +38,9 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# supabase CLI local dev (generated, project-machine-specific)
|
||||
/supabase/.branches
|
||||
/supabase/.temp
|
||||
/supabase/snippets
|
||||
|
||||
# scratch output of `npm run types:generate`, for comparing against the
|
||||
# hand-written lib/supabase/types.ts — never itself imported
|
||||
/lib/supabase/types.generated.ts
|
||||
|
||||
# local scratch scripts/screenshots (ad-hoc verification against a real or
|
||||
# seeded DB — can carry HR data or use SUPABASE_SERVICE_ROLE_KEY; never commit)
|
||||
# Kladden und Bildschirmfotos aus der Handprüfung — können Personendaten
|
||||
# oder Zugangsdaten enthalten; nie committen.
|
||||
.scratch_*
|
||||
/.scratch_shots/
|
||||
|
||||
@@ -56,5 +48,3 @@ next-env.d.ts
|
||||
.backups/
|
||||
|
||||
# Datenabzuege aus dem Umzug – enthalten Personendaten
|
||||
supabase-daten-*.sql
|
||||
supabase-voll-*.sql
|
||||
|
||||
@@ -15,12 +15,10 @@ eine `profiles`-Zeile braucht (siehe [docs/entra-sso.md](docs/entra-sso.md)).
|
||||
- **Containerisiert:** die Next.js-App (`Dockerfile`) **und die Datenbank**
|
||||
(Dienst `db`, `postgres:17-alpine`). Beide zusammen in
|
||||
`docker-compose.yml`; die Daten liegen im benannten Volume `db-daten`.
|
||||
- **Nicht mehr Supabase.** Die Anwendung sprach ohnehin unmittelbar mit
|
||||
PostgreSQL — Supabase war nur der Betreiber. Was die Plattform beisteuerte,
|
||||
bringt jetzt `deploy/db-init/` mit: die Anwendungsrolle ohne BYPASSRLS, die
|
||||
zwei benutzten Erweiterungen und eine Attrappe des `auth`-Schemas, die nur
|
||||
die alten Migrationen von Juni brauchen. Der Umzug steht in
|
||||
[Abschnitt 3a](#3a-umzug-von-supabase).
|
||||
- **Die Datenbank gehört zum Projekt.** Was eine gehostete Plattform sonst
|
||||
beisteuert, bringt `deploy/db-init/` mit: die Anwendungsrolle ohne
|
||||
BYPASSRLS, die zwei benutzten Erweiterungen und eine Attrappe des
|
||||
`auth`-Schemas, die nur die Migrationen von Juni 2026 brauchen.
|
||||
- Läuft die Datenbank woanders (Azure Flexible Server, RDS, eigenes Blech),
|
||||
genügt es, `DATABASE_URL` dorthin zeigen zu lassen und den `db`-Dienst nicht
|
||||
zu starten. Die Anwendung merkt keinen Unterschied.
|
||||
@@ -152,41 +150,6 @@ docker compose up -d --build
|
||||
`migrate` steht im Profil `tools` und läuft bei `docker compose up` nicht mit.
|
||||
Auf dem Host wird dafür weder Node noch psql gebraucht — nur Docker.
|
||||
|
||||
## 3a. Umzug von Supabase
|
||||
|
||||
Einmalig, wenn der Bestand noch bei Supabase liegt:
|
||||
|
||||
```bash
|
||||
SUPABASE_DB_URL='postgresql://postgres:<passwort>@<projekt>.supabase.com:5432/postgres' \
|
||||
./scripts/umzug-von-supabase.sh
|
||||
```
|
||||
|
||||
Was das Skript tut und warum:
|
||||
|
||||
1. **Schema aus den Migrationen**, nicht aus einem Abzug. Nachgewiesen ist,
|
||||
dass alle Migrationen auf einer leeren Datenbank durchlaufen und dabei
|
||||
Spalte für Spalte, Index für Index, Policy für Policy dasselbe ergeben wie
|
||||
die gewachsene Produktion. Der Weg hat zwei Vorteile: die Buchführung
|
||||
stimmt danach von selbst, und Supabase-eigene Rechte und Eigentümer kommen
|
||||
gar nicht erst mit.
|
||||
2. **Nur die Daten** werden abgezogen (`pg_dump --data-only
|
||||
--disable-triggers`). Ohne `--disable-triggers` stolpert jede
|
||||
Fremdschlüsselprüfung über die Ladereihenfolge. RLS steht nicht im Weg —
|
||||
keine der 19 Tabellen hat `FORCE ROW LEVEL SECURITY`, der Eigentümer
|
||||
schreibt also durch.
|
||||
3. Am Ende werden die Zeilenzahlen ausgegeben. **Mit denen bei Supabase
|
||||
vergleichen**, bevor irgendetwas abgeschaltet wird.
|
||||
|
||||
Der Abzug bleibt als Datei liegen. Erst löschen, wenn die Anwendung gegen die
|
||||
neue Datenbank nachweislich läuft.
|
||||
|
||||
Danach in `.env`:
|
||||
|
||||
```
|
||||
DATABASE_URL=postgresql://alpenwerk_app:<APP_DB_PASSWORD>@db:5432/alpenwerk
|
||||
DATABASE_SSL=false
|
||||
```
|
||||
|
||||
### Was am `auth`-Schema übrigbleibt
|
||||
|
||||
`deploy/db-init/01-auth-attrappe.sql` legt ein leeres `auth`-Schema an. Es wird
|
||||
@@ -283,8 +246,8 @@ und löscht sie danach wieder.
|
||||
|
||||
| Secret | Anmerkung |
|
||||
|---|---|
|
||||
| `DATABASE_URL` | Transaktions-Pooler (Supabase: 6543) — den benutzt die Anwendung |
|
||||
| `SUPABASE_DB_URL` | **Direkter** Zugang (Supabase: 5432) — nur für die Migrationen |
|
||||
| `DATABASE_URL` | Verbindung der Anwendungsrolle — die benutzt die Anwendung |
|
||||
| `MIGRATE_DATABASE_URL` | Zugang als Verwalter — nur für die Migrationen |
|
||||
| `AUTH_SECRET` | |
|
||||
| `AUTH_URL` | z. B. `https://hr.elycon.solutions` |
|
||||
| `AUTH_MICROSOFT_ENTRA_ID_ID` | |
|
||||
@@ -332,9 +295,9 @@ node --env-file=.env scripts/migrate.mjs --dry-run # was stünde an
|
||||
node --env-file=.env scripts/migrate.mjs # anwenden
|
||||
```
|
||||
|
||||
Buch geführt wird in `supabase_migrations.schema_migrations`, derselben Tabelle
|
||||
in derselben Form, die die Supabase-CLI benutzt — `supabase db push` von der
|
||||
Arbeitsstation bleibt damit möglich und überspringt, was hier schon lief.
|
||||
Buch geführt wird in `migrationen.schema_migrations`: eine Zeile je
|
||||
angewendeter Datei, mit ihrem Text. Der Läufer wendet nur an, was dort
|
||||
fehlt.
|
||||
|
||||
> **Einmalig, im August 2026 bereits erledigt:** Die Migrationen wurden bis
|
||||
> dahin von Hand eingespielt, die Buchführungstabelle existierte gar nicht. Ein
|
||||
@@ -409,9 +372,6 @@ Single-Instance-Compose-Konfiguration ist das nicht nötig.
|
||||
- **Cron läuft nicht:** `docker compose logs cron` – prüft, ob
|
||||
`/etc/crontabs/root` korrekt geschrieben wurde und ob `CRON_SECRET` in
|
||||
`.env` gesetzt ist (leer/fehlend führt serverseitig zu `401`).
|
||||
- **`max clients reached in session mode`:** der Verbindungsstring zeigt auf
|
||||
den Sitzungs-Modus des Poolers. Auf den Transaktions-Modus wechseln (bei
|
||||
Supabase Port 6543).
|
||||
- **Healthcheck rot:** `docker compose logs app` – meist `DATABASE_URL`
|
||||
fehlend oder nicht erreichbar. Der Pool baut die Verbindung erst beim
|
||||
ersten Zugriff auf, der Fehler steht deshalb im Log der Anfrage, nicht im
|
||||
|
||||
63
README.md
63
README.md
@@ -24,11 +24,13 @@ auf explizit aktivierte HR-Benutzer:innen beschränkt (siehe
|
||||
`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.
|
||||
- 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 ein lokales Supabase)
|
||||
(gegen eine erreichbare Datenbank)
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -38,14 +40,14 @@ cp .env.example .env.local # Werte eintragen, siehe unten
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Für lokale Supabase-Entwicklung (statt gegen ein Cloud-Projekt):
|
||||
Für eine lokale Datenbank genügt der Container aus `docker-compose.yml`:
|
||||
|
||||
```bash
|
||||
supabase start # startet lokalen Postgres/Auth/Studio-Stack
|
||||
docker compose up -d db
|
||||
docker compose run --rm migrate # spielt db/migrations/ ein
|
||||
```
|
||||
|
||||
`supabase/config.toml` und `.env.test.local` sind bereits auf die
|
||||
Standard-Ports der lokalen Supabase-CLI abgestimmt.
|
||||
Danach zeigt `DATABASE_URL` in `.env.local` auf diese Datenbank.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
@@ -77,8 +79,9 @@ selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut,
|
||||
| `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: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
|
||||
@@ -115,23 +118,24 @@ selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut,
|
||||
- Fehlt `CRON_SECRET` oder stimmt der Header nicht, antwortet die Route mit
|
||||
`401` (nicht `500` — bewusst, siehe `tests/unit/security.test.ts`).
|
||||
|
||||
## Supabase-Hinweise
|
||||
## Datenbank
|
||||
|
||||
- Schema-Quelle der Wahrheit: `supabase/migrations/`. Menschlich lesbare
|
||||
Fassung, aus der laufenden Datenbank erzeugt:
|
||||
- Schema-Quelle der Wahrheit: `db/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.
|
||||
- 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 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: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
|
||||
@@ -141,15 +145,18 @@ Reverse-Proxy/TLS, der nächtliche Lauf, Migrationen und Updates.
|
||||
|
||||
## 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.
|
||||
- **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
|
||||
|
||||
@@ -6,7 +6,7 @@ import { withUser } from "@/lib/db";
|
||||
import { callFunction, runMutation, type ActionResult, type MutationFn } from "@/lib/db/rpc";
|
||||
import { OFFBOARDING_PUNKTE } from "@/lib/offboarding";
|
||||
import { ONBOARDING_PUNKTE } from "@/lib/onboarding";
|
||||
import type { CollectiveAgreement, DienstwagenArt, NoteCategory, RelationshipType, Weekday, WorkerType } from "@/lib/supabase/types";
|
||||
import type { CollectiveAgreement, DienstwagenArt, NoteCategory, RelationshipType, Weekday, WorkerType } from "@/lib/types";
|
||||
|
||||
async function callRpc(fn: MutationFn, payload: Record<string, unknown>, revalidate: string[]): Promise<ActionResult> {
|
||||
const result = await runMutation(await currentUserId(), fn, payload);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { derivedStatusFilter } from "@/lib/employee-status-filter";
|
||||
import { fmtDate, fmtName, todayIso } from "@/lib/format";
|
||||
import { breadcrumbLabel, divisionOf, loadOrgMaps, subtreeOf, unitOf, type OrgEb } from "@/lib/org";
|
||||
import { loadPlacements } from "@/lib/placement";
|
||||
import type { EmploymentStatus } from "@/lib/supabase/types";
|
||||
import type { EmploymentStatus } from "@/lib/types";
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button, LINK_BUTTON_CLASS } from "@/components/ui/Button";
|
||||
// Without this file a failed render drops the user on Next.js's own error
|
||||
// screen — no navigation, no way back, and in production just "a client-side
|
||||
// exception occurred". `reset()` re-renders the segment, which is enough for
|
||||
// the common case of a transient Supabase timeout.
|
||||
// the common case of a transient database timeout.
|
||||
export default function AppError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
useEffect(() => {
|
||||
console.error("Route error:", error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Every page in this group is server-rendered per request (they all read
|
||||
// from Supabase), so without this the browser sits on the previous page with
|
||||
// from the database), so without this the browser sits on the previous page with
|
||||
// no feedback until the server answers — on the employee list, long enough
|
||||
// to look broken.
|
||||
export default function Loading() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { callFunction } from "@/lib/db/rpc";
|
||||
|
||||
// Applies effective-dated changes (Versetzung/Beförderung/Karenz/Reorg/Daten
|
||||
// ändern with a future "Wirksam ab" date) once their date has arrived — see
|
||||
// apply_due_pending_changes() in supabase/migrations.
|
||||
// apply_due_pending_changes() in db/migrations.
|
||||
//
|
||||
// Gerufen wird das vom `cron`-Dienst aus docker-compose.yml, täglich um 03:00.
|
||||
// Nicht im Namen einer HR-Person: es gibt keine angemeldete Sitzung, deshalb
|
||||
|
||||
@@ -9,7 +9,7 @@ import { deriveStatusAsOf, parseIsoDateParam, parseStatuses, type OrgLookups } f
|
||||
import { applyCriteria, loadDependentsCounts, loadOrgLookups, type ReportFilters } from "@/lib/reports-data";
|
||||
import { requireHrUser } from "@/lib/auth/require-hr";
|
||||
import { withUser } from "@/lib/db";
|
||||
import type { Database, Weekday } from "@/lib/supabase/types";
|
||||
import type { Database, Weekday } from "@/lib/types";
|
||||
|
||||
// Die Rohzeile plus die Einordnung, die nicht mehr auf ihr steht: sie kommt
|
||||
// über die Planstelle und die abgeleitete Berichtslinie.
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useState } from "react";
|
||||
import { AenderungsTabelle } from "@/components/ui/AenderungsTabelle";
|
||||
import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { actionBadgeStyle } from "@/lib/colors";
|
||||
import type { AuditChange } from "@/lib/supabase/types";
|
||||
import type { AuditChange } from "@/lib/types";
|
||||
|
||||
// Eine Protokollzeile zum Aufklappen.
|
||||
//
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SelectField, TextField } from "@/components/ui/Field";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { todayIso } from "@/lib/format";
|
||||
import type { RelationshipType } from "@/lib/supabase/types";
|
||||
import type { RelationshipType } from "@/lib/types";
|
||||
|
||||
const RELATIONSHIPS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"];
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { deleteEmployeeDependent } from "@/actions/employees";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { fmtDate, todayIso } from "@/lib/format";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
import { AddDependentModal } from "./AddDependentModal";
|
||||
|
||||
type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"];
|
||||
|
||||
@@ -11,7 +11,7 @@ import { fmtFullName, tenure } from "@/lib/format";
|
||||
import { fortschritt as fortschrittOffboarding, gehoertOffboarding } from "@/lib/offboarding";
|
||||
import { fortschritt as fortschrittOnboarding } from "@/lib/onboarding";
|
||||
import type { OpenPositionResolved } from "@/lib/positions";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
import { DatenAendernPanel } from "./panels/DatenAendernPanel";
|
||||
import { KarenzPanel } from "./panels/KarenzPanel";
|
||||
import { PromotePanel } from "./panels/PromotePanel";
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TextField } from "@/components/ui/Field";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { fmtDate } from "@/lib/format";
|
||||
import type { AuditChange } from "@/lib/supabase/types";
|
||||
import type { AuditChange } from "@/lib/types";
|
||||
|
||||
// Berichtigen, nicht neu erfassen.
|
||||
//
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SelectField, TextField } from "@/components/ui/Field";
|
||||
import type { CollectiveAgreement, DienstwagenArt, Weekday, WorkerType } from "@/lib/supabase/types";
|
||||
import type { CollectiveAgreement, DienstwagenArt, Weekday, WorkerType } from "@/lib/types";
|
||||
|
||||
const WEEKDAYS: Weekday[] = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { brauchtAufenthaltstitel, UN_COUNTRIES } from "@/lib/countries";
|
||||
import { STUNDEN_GRUENDE } from "@/lib/absence";
|
||||
import { fmtFullName, todayIso } from "@/lib/format";
|
||||
import { isValidSvnr, requiresAustrianSvnr } from "@/lib/svnr";
|
||||
import { EMERGENCY_RELATIONS, type ContractType, type Database, type EmploymentType, type GenderType } from "@/lib/supabase/types";
|
||||
import { EMERGENCY_RELATIONS, type ContractType, type Database, type EmploymentType, type GenderType } from "@/lib/types";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
type Dependent = Database["public"]["Tables"]["employee_dependents"]["Row"];
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { ABSENCE_TYPES, absenceLabel, RUECKKEHR_GRUENDE } from "@/lib/absence";
|
||||
import { fmtDate, fmtName } from "@/lib/format";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
type Mode = "adjust" | "return";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/Button";
|
||||
import { SelectField, TextField } from "@/components/ui/Field";
|
||||
import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import type { Database, PaygradeType } from "@/lib/supabase/types";
|
||||
import type { Database, PaygradeType } from "@/lib/types";
|
||||
import { fmtName } from "@/lib/format";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { fmtDate, fmtName } from "@/lib/format";
|
||||
import type { OpenPositionResolved } from "@/lib/positions";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/Button";
|
||||
import { SelectField, TextField, TextareaField } from "@/components/ui/Field";
|
||||
import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
import { fmtDate, fmtName } from "@/lib/format";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SelectField, TextField } from "@/components/ui/Field";
|
||||
import { SlideOver } from "@/components/ui/SlideOver";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import type { OpenPositionResolved } from "@/lib/positions";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
import { fmtName } from "@/lib/format";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SegmentedControl } from "@/components/ui/SegmentedControl";
|
||||
import { actionBadgeStyle } from "@/lib/colors";
|
||||
import { fmtDate, todayIso } from "@/lib/format";
|
||||
import { darfBearbeitetWerden, darfKorrigiertWerden, loeschVorschau } from "@/lib/history";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
|
||||
type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"];
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SelectField, TextField, TextareaField } from "@/components/ui/Field";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { NOTE_CATEGORY_STYLES } from "@/lib/colors";
|
||||
import { fmtDate } from "@/lib/format";
|
||||
import type { Database, NoteCategory } from "@/lib/supabase/types";
|
||||
import type { Database, NoteCategory } from "@/lib/types";
|
||||
|
||||
type Note = Database["public"]["Tables"]["employee_notes"]["Row"];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AngehoerigeSection } from "@/components/employees/AngehoerigeSection";
|
||||
import { brauchtAufenthaltstitel } from "@/lib/countries";
|
||||
import { fmtAge, fmtDate } from "@/lib/format";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
type Location = Database["public"]["Tables"]["locations"]["Row"];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dienstwagenLabel } from "@/lib/dienstwagen";
|
||||
import { fmtDate } from "@/lib/format";
|
||||
import type { Database } from "@/lib/supabase/types";
|
||||
import type { Database } from "@/lib/types";
|
||||
|
||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/Button";
|
||||
import { SelectField, TextField } from "@/components/ui/Field";
|
||||
import { fmtDate } from "@/lib/format";
|
||||
import { formatSvnr, svnrErrorMessage, validateSvnr } from "@/lib/svnr";
|
||||
import type { RelationshipType } from "@/lib/supabase/types";
|
||||
import type { RelationshipType } from "@/lib/types";
|
||||
import type { HireDraftAngehoerige, HireDraftData } from "./types";
|
||||
|
||||
const VERHAELTNIS: RelationshipType[] = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SelectField, TextField } from "@/components/ui/Field";
|
||||
import { EMERGENCY_RELATIONS } from "@/lib/supabase/types";
|
||||
import { EMERGENCY_RELATIONS } from "@/lib/types";
|
||||
import type { HireDraftData } from "./types";
|
||||
|
||||
// Eigener Schritt, kurz vor der Zusammenfassung.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RoleEmploymentFields } from "@/components/employees/RoleEmploymentFields";
|
||||
import { SelectField, TextField } from "@/components/ui/Field";
|
||||
import type { PaygradeType } from "@/lib/supabase/types";
|
||||
import type { PaygradeType } from "@/lib/types";
|
||||
import type { HireDraftData } from "./types";
|
||||
|
||||
const PAYGRADES: { value: PaygradeType; label: string; description: string }[] = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CollectiveAgreement, ContractType, DienstwagenArt, EmploymentType, GenderType, PaygradeType, RelationshipType, Weekday, WorkerType } from "@/lib/supabase/types";
|
||||
import type { CollectiveAgreement, ContractType, DienstwagenArt, EmploymentType, GenderType, PaygradeType, RelationshipType, Weekday, WorkerType } from "@/lib/types";
|
||||
|
||||
// The spec's hire wizard field list (§4.4) omits Geschlecht and Standort even
|
||||
// though both are NOT NULL on employees — added here (defaults keep them
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
type ReportRow,
|
||||
type UnitOption,
|
||||
} from "@/lib/reports";
|
||||
import type { HistoryEventType } from "@/lib/supabase/types";
|
||||
import type { HistoryEventType } from "@/lib/types";
|
||||
|
||||
const SPLIT_COLORS = ["bg-brand-500", "bg-info-text", "bg-purple-text", "bg-warning-text", "bg-success-text", "bg-danger-solid"];
|
||||
const EVENT_TYPES = Object.keys(EVENT_TYPE_LABELS) as HistoryEventType[];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AuditChange } from "@/lib/supabase/types";
|
||||
import type { AuditChange } from "@/lib/types";
|
||||
|
||||
// Was sich geändert hat, feldweise — im Protokoll und in der Historie einer
|
||||
// Person dieselbe Darstellung. Zwei Ansichten derselben Sache verschieden zu
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { absenceLabel } from "@/lib/absence";
|
||||
import { STATUS_STYLES } from "@/lib/colors";
|
||||
import { fmtDate } from "@/lib/format";
|
||||
import type { EmploymentStatus } from "@/lib/supabase/types";
|
||||
import type { EmploymentStatus } from "@/lib/types";
|
||||
|
||||
type StatusChipProps = {
|
||||
status: EmploymentStatus;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user