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:
117
db/migrations/20260731090000_app_upsert_user.sql
Normal file
117
db/migrations/20260731090000_app_upsert_user.sql
Normal file
@@ -0,0 +1,117 @@
|
||||
-- Schritt 2: die Anmeldung braucht einen Weg, ihre Zeile in app_users
|
||||
-- anzulegen — bevor es einen Sitzungskontext gibt.
|
||||
--
|
||||
-- Das ist das Henne-Ei-Problem jeder eigenen Anmeldung: app.user_id kann erst
|
||||
-- gesetzt werden, wenn die Kennung feststeht, und die entsteht genau hier.
|
||||
-- Bisher löste ein Dienstschlüssel mit BYPASSRLS solche Fälle. Den gibt es
|
||||
-- nicht mehr, und er soll auch nicht zurückkommen — eine Verbindung, die
|
||||
-- alles darf, ist für einen einzigen Schreibvorgang ein zu grosser Hebel.
|
||||
--
|
||||
-- Stattdessen: eine SECURITY-DEFINER-Funktion mit genau einer Befugnis.
|
||||
-- Sie schreibt in app_users und liest lesend in profiles — sonst nichts. Wer
|
||||
-- sie aufruft, bekommt eine UUID zurück und sonst keine Auskunft.
|
||||
create or replace function app_upsert_user(
|
||||
p_external_id text,
|
||||
p_email text,
|
||||
p_full_name text
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public, pg_temp
|
||||
as $$
|
||||
declare
|
||||
v_id uuid;
|
||||
begin
|
||||
if p_external_id is null or btrim(p_external_id) = '' then
|
||||
raise exception 'Externe Kennung fehlt.';
|
||||
end if;
|
||||
if p_email is null or btrim(p_email) = '' then
|
||||
raise exception 'E-Mail-Adresse fehlt.';
|
||||
end if;
|
||||
|
||||
-- Bekanntes Konto: nur nachziehen, was sich beim Anbieter geändert haben
|
||||
-- kann. Die Kennung bleibt, auch wenn Name oder Adresse wechseln — daran
|
||||
-- hängen Notizen, Entwürfe und Protokolleinträge.
|
||||
update app_users
|
||||
set email = p_email,
|
||||
full_name = coalesce(p_full_name, full_name),
|
||||
last_seen_at = now()
|
||||
where external_id = p_external_id
|
||||
returning id into v_id;
|
||||
|
||||
if v_id is not null then
|
||||
return v_id;
|
||||
end if;
|
||||
|
||||
-- Erstanmeldung. Gibt es zu dieser Adresse bereits ein Profil, wird dessen
|
||||
-- Kennung übernommen statt einer neuen: profiles.id ist heute die
|
||||
-- auth.users.id, und neun Fremdschlüssel zeigen darauf. Eine frisch
|
||||
-- vergebene UUID würde die Person von ihrer eigenen Vorgeschichte trennen
|
||||
-- — sie wäre angemeldet, hätte aber weder Rolle noch Freischaltung.
|
||||
--
|
||||
-- Der Abgleich über die Adresse ist hier vertretbar und sonst nirgends:
|
||||
-- die Adresse kommt aus einem von Entra ausgestellten Token, nicht aus
|
||||
-- einem Formular. Wer sie behauptet, hat sie bereits bewiesen.
|
||||
select p.id into v_id
|
||||
from profiles p
|
||||
where lower(p.email) = lower(p_email)
|
||||
limit 1;
|
||||
|
||||
v_id := coalesce(v_id, gen_random_uuid());
|
||||
|
||||
begin
|
||||
insert into app_users (id, external_id, email, full_name, last_seen_at)
|
||||
values (v_id, p_external_id, p_email, p_full_name, now());
|
||||
exception
|
||||
when unique_violation then
|
||||
-- Zwei gleichzeitige Erstanmeldungen desselben Kontos. Die zweite
|
||||
-- findet die Zeile, die die erste gerade angelegt hat.
|
||||
select id into v_id from app_users where external_id = p_external_id;
|
||||
if v_id is null then
|
||||
raise;
|
||||
end if;
|
||||
end;
|
||||
|
||||
return v_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
comment on function app_upsert_user(text, text, text) is
|
||||
'Legt die app_users-Zeile zur Erstanmeldung an und liefert die Kennung. Übernimmt bei bekannter E-Mail die vorhandene profiles.id.';
|
||||
|
||||
-- app_users trägt RLS und hat bewusst keine Schreib-Policy: die Anwendung
|
||||
-- kommt an die Tabelle nur durch diese Funktion. Ein Fehler im Anwendungscode
|
||||
-- kann dort also nichts anlegen, ändern oder löschen.
|
||||
do $$
|
||||
declare r text;
|
||||
begin
|
||||
foreach r in array array['anon', 'authenticated', 'service_role'] loop
|
||||
if exists (select 1 from pg_roles where rolname = r) then
|
||||
execute format('grant execute on function app_upsert_user(text, text, text) to %I', r);
|
||||
end if;
|
||||
end loop;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ═══ Gegenprobe ══════════════════════════════════════════════════
|
||||
-- Zweimal dieselbe externe Kennung muss dieselbe UUID ergeben. Wäre es nicht
|
||||
-- so, bekäme jede Anmeldung ein neues Konto und niemand behielte seine
|
||||
-- Rolle — ein Fehler, der sich erst Wochen später als „meine Notizen sind
|
||||
-- weg" zeigt.
|
||||
do $$
|
||||
declare
|
||||
v_first uuid;
|
||||
v_second uuid;
|
||||
v_probe text := 'probe-' || gen_random_uuid()::text;
|
||||
begin
|
||||
v_first := app_upsert_user(v_probe, v_probe || '@example.invalid', 'Probe');
|
||||
v_second := app_upsert_user(v_probe, v_probe || '@example.invalid', 'Probe');
|
||||
|
||||
if v_first is distinct from v_second then
|
||||
raise exception 'app_upsert_user() vergibt bei zweiter Anmeldung eine neue Kennung — Abbruch.';
|
||||
end if;
|
||||
|
||||
delete from app_users where id = v_first;
|
||||
end;
|
||||
$$;
|
||||
Reference in New Issue
Block a user