-- 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; $$;