diff --git a/supabase/migrations/20260805140000_profiles_reference_app_users.sql b/supabase/migrations/20260805140000_profiles_reference_app_users.sql new file mode 100644 index 0000000..edd16db --- /dev/null +++ b/supabase/migrations/20260805140000_profiles_reference_app_users.sql @@ -0,0 +1,104 @@ +-- Die Fremdschlüssel von auth.users auf app_users umhängen. +-- +-- Solange profiles.id auf auth.users zeigt, lässt sich **keine zweite Person +-- freischalten**. Die Anmeldung läuft über Auth.js und legt dort nichts mehr +-- an; wer sich neu anmeldet, bekommt eine app_users-Zeile, aber die dazu +-- nötige profiles-Zeile scheitert am Fremdschlüssel. Das Ergebnis wäre +-- „Kein HR-Zugriff" ohne Möglichkeit, es zu ändern. +-- +-- Damit ist das hier keine Aufräumarbeit, sondern die Voraussetzung dafür, +-- dass die Anwendung mehr als eine Person bedienen kann. +-- +-- Vorher geprüft: jeder referenzierte Wert steht bereits in app_users. Die +-- Umhängung ändert also keine Daten, nur die Zusicherung. + +-- ═══ 1. Sicherheitsnetz ══════════════════════════════════════════ +-- Was in app_users fehlt, wird aus profiles ergänzt. Im geprüften Bestand +-- ist das leer; die Migration soll aber auch auf einer Kopie laufen, in der +-- jemand zwischenzeitlich etwas angelegt hat. +insert into app_users (id, external_id, email, full_name) +select p.id, 'legacy:' || p.id::text, p.email, p.full_name + from profiles p + where not exists (select 1 from app_users a where a.id = p.id) +on conflict (id) do nothing; + +-- ═══ 2. Umhängen ═════════════════════════════════════════════════ +-- Über den Katalog statt acht handgeschriebene Anweisungen: die Namen der +-- Zwänge stammen aus verschiedenen Migrationen, und einer davon von Hand +-- falsch abgeschrieben hiesse, dass er stehen bleibt. +do $$ +declare + r record; + v_delete text; +begin + for r in + select k.conname, c.relname as tabelle, a.attname as spalte, k.confdeltype + from pg_constraint k + join pg_class c on c.oid = k.conrelid + join pg_namespace n on n.oid = c.relnamespace + join pg_attribute a on a.attrelid = k.conrelid and a.attnum = any(k.conkey) + where k.contype = 'f' + and n.nspname = 'public' + and k.confrelid = ( + select oid from pg_class + where relname = 'users' + and relnamespace = (select oid from pg_namespace where nspname = 'auth') + ) + loop + -- Das Löschverhalten bleibt, wie es war: profiles hängt kaskadierend am + -- Konto, die Protokoll- und Notizfelder nicht — dort soll ein Eintrag + -- gerade nicht verschwinden, weil ein Konto entfernt wird. + v_delete := case r.confdeltype when 'c' then ' on delete cascade' else '' end; + + execute format('alter table %I drop constraint %I', r.tabelle, r.conname); + execute format('alter table %I add constraint %I foreign key (%I) references app_users(id)%s', + r.tabelle, r.conname, r.spalte, v_delete); + raise notice '%.% -> app_users%', r.tabelle, r.spalte, v_delete; + end loop; +end; +$$; + +-- ═══ 3. Gegenprobe ═══════════════════════════════════════════════ +do $$ +declare + v_offen int; + v_neu int; +begin + select count(*) into v_offen + from pg_constraint k + join pg_class c on c.oid = k.conrelid + join pg_namespace n on n.oid = c.relnamespace + where k.contype = 'f' and n.nspname = 'public' + and k.confrelid = ( + select oid from pg_class + where relname = 'users' + and relnamespace = (select oid from pg_namespace where nspname = 'auth') + ); + if v_offen > 0 then + raise exception '% Fremdschlüssel zeigen weiterhin auf auth.users.', v_offen; + end if; + + select count(*) into v_neu + from pg_constraint k + join pg_class c on c.oid = k.conrelid + join pg_namespace n on n.oid = c.relnamespace + where k.contype = 'f' and n.nspname = 'public' + and k.confrelid = 'app_users'::regclass; + if v_neu < 8 then + raise exception 'Nur % Fremdschlüssel zeigen auf app_users — erwartet mindestens 8.', v_neu; + end if; + + -- Und die eigentliche Frage: lässt sich jetzt eine zweite Person anlegen? + -- Geprüft und wieder entfernt, damit die Migration keine Daten hinterlässt. + declare + v_id uuid := gen_random_uuid(); + begin + insert into app_users (id, external_id, email, full_name) + values (v_id, 'probe:' || v_id::text, 'probe@example.invalid', 'Probe'); + insert into profiles (id, email, full_name, role, is_active) + values (v_id, 'probe@example.invalid', 'Probe', 'hr', false); + delete from profiles where id = v_id; + delete from app_users where id = v_id; + end; +end; +$$;