b9da3f341122f2f89a726f845900658ef8ed8103
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| b87c8ad64c |
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> |
|||
| 4d8e3f7154 |
Restore the shapes the application was written against
The app runs against a real database for the first time since the port, and two things were broken. Both were invisible to typecheck, lint, 192 tests and the build. Sign-in looped. Auth.js created the session and app_upsert_user() adopted the existing profiles id correctly — the row was in app_users, right id and all — but the proxy builds its own Auth.js instance from lib/auth/config.ts alone, and the session callback that copies token.uid onto session.user.id lived in auth.ts. So the proxy saw a session without an id, treated every signed-in user as signed out, and sent them back to /login. Click, flash, login page: from the outside it looked like the button did nothing. The callback moves to the config both instances share. auth.ts now spreads the base callbacks instead of replacing them, which is the mistake that would reintroduce this. The proxy test did not catch it because its fixture hands the handler a session that already has user.id — it tested the routing, not the shape Auth.js actually produces. Then the dashboard crashed on a.date.localeCompare. PostgREST returned JSON: a `date` arrived as "2026-08-03", a `numeric` as a number, and that is what lib/supabase/types.ts declares and what every sort, every date comparison and every status derivation assumes. The pg driver does the opposite — Date object and string respectively. The declarations stayed true to what the code believes; only the runtime value changed, which is why nothing flagged it. The driver is configured back to the declared shapes in lib/db/pool.ts, rather than rewriting 49 call sites. That also removes a timezone hazard: `date` is a calendar day, and as a Date object it acquires midnight in the server's zone — a birth date would shift by a day in Austria, always. The same class of bug as in the seed. int8 stays a string on purpose: it only comes from count() and is read through Number() everywhere; parsed as a number it would quietly lose precision past 2^53. A missing sign-in error now reaches the server log. Auth.js was failing silently — a 302 back to /login and nothing to read. That was its own defect, and it is the reason the first diagnosis took as long as it did. Verified against the live database: all six pages render, 797 active of 852 records, 744.4 FTE, and a detail page shows birth date 15.08.1968 against SV number 7960 150868 — the digits agree, so no day has shifted. Both new tests were checked by mutation: remove the fix and they fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| a66263a96e |
Put the session context under the app's own control
Erster Schritt weg von Supabase hin zu "läuft auf jedem PostgreSQL".
Gemessen sitzt die Kopplung nicht dort, wo der Begriff "Supabase-Projekt"
sie vermuten lässt: das Schema ist reines PostgreSQL, und von 58 RLS-Policies
rufen nur fünf auth.uid() direkt auf. Die übrigen 53 gehen über is_hr_user().
Diese eine Funktion ist die Brücke — wird sie umgelegt, folgt der Rest.
Die Migration legt sie um. app_current_user_id() liest jetzt zuerst
current_setting('app.user_id') und fällt nur ersatzweise auf auth.uid()
zurück. Deshalb plpgsql statt language sql: eine SQL-Funktion wird beim
Anlegen geparst, und auth.uid() gibt es auf einem gewöhnlichen PostgreSQL
nicht — die Migration liesse sich dort gar nicht erst anwenden. Der
Ausnahmeblock fängt das ab, und damit läuft dieselbe Migration auf beiden
Systemen. Der Rückfall verschwindet mit der Abschlussmigration.
Dazu app_users als Nachfolger von auth.users, external_id ist die oid des
Anbieters statt der E-Mail: eine Namensänderung darf kein zweites Konto
erzeugen.
Die neue Zugriffsschicht ist Kysely auf einem pg-Pool. Was daran zählt, ist
nicht der Query-Builder, sondern was er verhindert:
- Die Kysely-Instanz wird nicht exportiert. Wer abfragen will, geht durch
withUser() — und das öffnet immer eine Transaktion.
- set_config(..., true) ist transaktionslokal. Ohne das dritte Argument
bliebe die Kennung an der gepoolten Verbindung kleben und die nächste
Anfrage liefe im Namen der vorherigen Person. In einer Personaldatenbank.
- Eine ESLint-Regel verbietet den Import von pg und von lib/db/pool
ausserhalb von lib/db. Nachgewiesen: eine Testdatei mit beiden Importen
erzeugt zwei Fehler.
- Einen privilegierten Zugang gibt es nicht mehr. asSystem() benutzt
dieselbe Rolle ohne BYPASSRLS; was ohne angemeldete Person laufen darf,
muss als SECURITY-DEFINER-Funktion in der Datenbank stehen.
tests/integration/session-context.test.ts läuft gegen einen Pool mit genau
einer Verbindung — sonst träfe er die Lücke mal und mal nicht. Er prüft, dass
nach Commit *und* nach Rollback nichts an der Verbindung zurückbleibt, und
belegt in einer Gegenprobe, dass eine Einstellung ohne Transaktion tatsächlich
hängen bleibt. Ein Sicherheitstest, der sich mangels DATABASE_URL selbst
überspringt, wäre schlimmer als keiner: in der CI schlägt schon das Fehlen
des Verbindungsstrings fehl.
Beim Schreiben der Migration stellte sich heraus, dass die Policies
hire_drafts_owner und saved_reports_owner heissen, nicht _own. Mit dem
geratenen Namen hätte drop policy nichts getroffen und create policy wäre mit
"already exists" abgebrochen.
Typecheck, Lint und 182 Tests sind grün. Die Anwendung läuft unverändert
weiter — sie benutzt die neue Schicht noch nicht.
|
|||
| ef9852b09c |
Phase 1: project foundation for Alpenwerk HR
Scaffolds the Next.js 16 / TypeScript strict / Tailwind v3 app per NEXTJS_REBUILD_SUPERPROMPT.md, and implements the Foundation slice from the Phase 1 plan: - Corrected Supabase schema (supabase/schema.sql): org units, employees, history, positions, hire drafts, saved reports, audit log, reorg scenarios, role-based profiles, salary-masking view, RLS policies, auto-derivation triggers, position-number generator. - Seed script (supabase/seed.ts): ~800 realistic Austrian employees across 9 divisions / 16 departments / 35 teams, history, 8 open positions, and hr_admin/manager test accounts. - Supabase clients (lib/supabase/*), design tokens (tailwind.config.ts), format/color helpers (lib/format.ts, lib/colors.ts). - Shared UI kit (components/ui): Avatar, StatusChip, Toast, Modal, SlideOver, SegmentedControl, Lookup. - Auth (login page, Server Actions) and proxy.ts (Next 16's replacement for middleware) guarding the authenticated route group. - Shell (Sidebar, Topbar, NewHireButton stub) and the Dashboard page, reading live data via employees_directory. Employees list/detail, hire wizard, action panels, org chart, positions, reports, and audit log are deferred to later phases per the plan. |