Let a second person be activated at all
profiles.id referenced auth.users. Sign-in goes through Auth.js now and creates nothing there, so a new colleague could sign in, receive an app_users row, and then be impossible to authorise: the profiles row needed to grant HR access could not be inserted. She would see "Kein HR-Zugriff" with no way to change it. All eight foreign keys in the public schema now point at app_users, walked from the catalogue rather than written out — their names come from different migrations and one transcribed wrongly means it silently stays behind. The delete behaviour is preserved: profiles still cascades from the account, audit and note fields do not, because an entry must not vanish when an account is removed. Every referenced value was already present in app_users, so nothing moved; only the guarantee changed. A backfill from profiles runs first anyway, for copies of this database where someone created something in between. The check at the end does the thing that matters: it creates a second account with a profile and removes it again. Counting constraints would have passed while the actual case still failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user