-- KTO-01 Registrierung: strengere Regeln beim Anlegen des Profils (Spec #1, § 3.2). -- Nur additiv: bestehende Migrationen bleiben unverändert. -- -- 1. display_name: kein Fallback auf das E-Mail-Präfix, kein stilles Kürzen; Länge 1-30 Codepoints -- nach btrim. Verstoß -> Registrierung scheitert mit 'display_name_invalid'. -- 2. terms_version: Version der zugestimmten Rechtstexte (Nachweis); Pflicht für neue Konten. -- 3. locale: Standard und Trigger-Fallback 'en' (nur 'de'/'en' erlaubt). -- 4. Sync-Trigger: profiles.locale -> auth.users.raw_user_meta_data.locale (Mailsprache). -- Anzeigename: Leerraum zählt nicht. char_length zählt Zeichen (Codepoints), nicht Bytes. -- NOT VALID + VALIDATE: der Constraint gilt sofort für neue Zeilen; VALIDATE prüft den Bestand -- (nur Entwicklungsdaten) mit schwächerem Sperrniveau und bricht die Migration ab, falls ein -- Bestandsname nur aus Leerzeichen besteht. alter table public.profiles add constraint profiles_display_name_not_blank check (char_length(btrim(display_name)) between 1 and 30) not valid; alter table public.profiles validate constraint profiles_display_name_not_blank; -- Version der Rechtstexte. Nullable nur wegen vorhandener Entwicklungszeilen; für neue Konten -- erzwingt handle_new_user() einen Wert. Die Spalte steht bewusst NICHT im Spalten-Grant -- "update" von 20261102100100_profiles.sql: der Nutzer kann sie nicht ändern. alter table public.profiles add column terms_version text check (terms_version is null or char_length(terms_version) between 1 and 50); -- Ohne Angabe gilt Englisch (QUE-05); der Client sendet immer die aktive Sprache. alter table public.profiles alter column locale set default 'en'; -- Profil beim Registrieren anlegen (ersetzt die Funktion aus 20261102100100_profiles.sql). -- security definer nötig: Der Trigger läuft im Kontext von auth.users (Rolle supabase_auth_admin), -- die Rolle hat keine Rechte auf public.profiles (RLS, keine Insert-Policy für Clients). -- search_path ist leer, alle Objekte sind schema-qualifiziert. create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = '' as $$ declare meta jsonb := coalesce(new.raw_user_meta_data, '{}'::jsonb); name text := btrim(meta ->> 'display_name'); version text := btrim(meta ->> 'terms_version'); begin if coalesce(meta ->> 'age_confirmed', '') <> 'true' then raise exception 'age_confirmation_required'; end if; if coalesce(meta ->> 'terms_accepted', '') <> 'true' then raise exception 'terms_acceptance_required'; end if; if version is null or char_length(version) not between 1 and 50 then raise exception 'terms_version_required'; end if; if name is null or char_length(name) not between 1 and 30 then raise exception 'display_name_invalid'; end if; insert into public.profiles (id, display_name, locale, terms_version, terms_accepted_at, age_confirmed_at) values ( new.id, name, case when meta ->> 'locale' in ('de', 'en') then meta ->> 'locale' else 'en' end, version, now(), now() ); return new; end; $$; -- Mailsprache folgt dem Profil: GoTrue-Vorlagen sehen nur user_metadata (.Data.locale). -- security definer nötig: Schreiben auf auth.users ist dem Client verwehrt. Der Trigger -- ändert ausschließlich die Zeile des eigenen Nutzers (where id = new.id) und nur den Schlüssel -- 'locale'. search_path ist leer. create or replace function public.sync_profile_locale_to_auth() returns trigger language plpgsql security definer set search_path = '' as $$ begin update auth.users set raw_user_meta_data = coalesce(raw_user_meta_data, '{}'::jsonb) || jsonb_build_object('locale', new.locale) where id = new.id; return new; end; $$; revoke all on function public.sync_profile_locale_to_auth() from public, anon, authenticated; create trigger sync_locale_to_auth after update of locale on public.profiles for each row when (old.locale is distinct from new.locale) execute function public.sync_profile_locale_to_auth();