diff --git a/docs/auth-konfiguration.md b/docs/auth-konfiguration.md index e8c09b4..82410f5 100644 --- a/docs/auth-konfiguration.md +++ b/docs/auth-konfiguration.md @@ -42,8 +42,6 @@ Spalte „Wirkung“: **lokal** = wirkt in der CLI über `config.toml`; **Env** | AUTH-CFG-24 | SMS-Autoconfirm aus | `SMS_AUTOCONFIRM=false` | lokal + Env | `auth.sms.enable_confirmations = true` / `GOTRUE_SMS_AUTOCONFIRM=false`; in der Self-Hosting-Vorlage `ENABLE_PHONE_AUTOCONFIRM=false` | SMS ist nicht im Einsatz, die Einstellung steuert aber den Enumerationsschutz beim E-Mail-Signup. Mit `true` (CLI-Standard und Vorlage `docker/.env.example`!) liefert Signup mit bestätigter Adresse 422 `user_already_exists` | 4 | | AUTH-CFG-25 | Passwortänderung ohne erneute Anmeldung | `false` (Standard, ausdrücklich gesetzt) | lokal + Env | `auth.email.secure_password_change` / `GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION` (Env-Name vor Release gegen das Image prüfen) | Recovery-Sitzungen sind ausgenommen; ob das aktuelle Passwort verlangt wird, entscheidet KTO-11 (N13) | (KTO-11) | -AUTH-CFG-25: `auth.email.secure_password_change = false` (Standard, ausdrücklich gesetzt). Recovery-Sitzungen sind davon ausgenommen; ob für die Passwortänderung das aktuelle Passwort verlangt wird, entscheidet KTO-11 (N13). - ## 2. Env-Block für Staging und Produktion (GoTrue, ohne Secrets) Gilt für das Image `supabase/gotrue:v2.197.0` (pinnen, nicht `latest`). Secrets (JWT-Secret, SMTP-Passwort, DB-URL) stehen nicht hier, sondern im Secret-Speicher des Hosting-Setups. @@ -64,6 +62,12 @@ GOTRUE_PASSWORD_MIN_LENGTH=10 GOTRUE_PASSWORD_REQUIRED_CHARACTERS= GOTRUE_PASSWORD_HIBP_ENABLED=false +# AUTH-CFG-23 (Stagingwert; in #1 nicht festgelegt, CLI-Standard true, Entscheidung in KTO-11) +GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=true + +# AUTH-CFG-25 (Env-Name vor Release gegen das Image prüfen) +GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=false + # AUTH-CFG-07, -08 GOTRUE_MAILER_OTP_EXP=3600 GOTRUE_SMTP_MAX_FREQUENCY=60s @@ -83,8 +87,8 @@ GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=10 # AUTH-CFG-17, -18 (Betreff je Sprache verzweigt, Text identisch zu config.toml; URLs siehe Abschnitt 3) GOTRUE_MAILER_TEMPLATES_CONFIRMATION=https:///confirmation.html GOTRUE_MAILER_TEMPLATES_RECOVERY=https:///recovery.html -GOTRUE_MAILER_SUBJECTS_CONFIRMATION={{ if and .Data (eq (index .Data "locale") "de") }}Bestätige deine E-Mail-Adresse für Shuffle{{ else }}Confirm your email address for Shuffle{{ end }} -GOTRUE_MAILER_SUBJECTS_RECOVERY={{ if and .Data (eq (index .Data "locale") "de") }}Passwort für Shuffle zurücksetzen{{ else }}Reset your Shuffle password{{ end }} +GOTRUE_MAILER_SUBJECTS_CONFIRMATION={{ if and .Data (eq (printf "%v" (index .Data "locale")) "de") }}Bestätige deine E-Mail-Adresse für Shuffle{{ else }}Confirm your email address for Shuffle{{ end }} +GOTRUE_MAILER_SUBJECTS_RECOVERY={{ if and .Data (eq (printf "%v" (index .Data "locale")) "de") }}Passwort für Shuffle zurücksetzen{{ else }}Reset your Shuffle password{{ end }} # AUTH-CFG-19 (Benachrichtigungen aus; KTO-11 schaltet email_changed ein) GOTRUE_MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED=false @@ -115,7 +119,7 @@ Getestet gegen CLI 3.0.0 und GoTrue `v2.197.0` über Kong (wie die App). Die Dro | Punkt | Ergebnis | |---|---| -| (a) Betreff und Vorlage werten `.Data.locale` aus | Bestätigt. `de` → deutsch; `en` oder fehlend → englisch. `index .Data "locale"` ist sicher, wenn der Schlüssel fehlt. Nach Profiländerung (Sync-Trigger) kommt die Mail in der neuen Sprache. | +| (a) Betreff und Vorlage werten `.Data.locale` aus | Bestätigt. `de` → deutsch; `en` oder fehlend → englisch. `printf "%v" (index .Data "locale")` ist sicher, wenn der Schlüssel fehlt oder kein String ist (sonst HTTP 500 bei `eq`). Nach Profiländerung (Sync-Trigger) kommt die Mail in der neuen Sprache. | | (b) `type` für `token_hash` | Bestätigung `email` (auch `signup`), Reset `recovery`. Zweites Einlösen → 403 `otp_expired`. | | (c) Limiter Passwort-Login | `GOTRUE_RATE_LIMIT_TOKEN_REFRESH` (150 je 5 min, Burst 30); Signup/Recover/Resend `GOTRUE_RATE_LIMIT_OTP` (30 je 5 min). **Greifen nur mit `GOTRUE_RATE_LIMIT_HEADER` und Requests mit dem Header.** Die lokale CLI kann den Header nicht setzen, also lokal keine Drosselung. 429 ohne `Retry-After`. | | (d) `email_sent` | Instanzweit. In der CLI wirkungslos (`360000`). Bei erschöpftem Limit antwortet `/recover` für bestehende Adressen 429, für unbekannte 200 (Seitenkanal, akzeptiert F12). | diff --git a/docs/specs/1-auth-registrierung-anmeldung.md b/docs/specs/1-auth-registrierung-anmeldung.md index e5bd31c..b8d43f1 100644 --- a/docs/specs/1-auth-registrierung-anmeldung.md +++ b/docs/specs/1-auth-registrierung-anmeldung.md @@ -490,4 +490,4 @@ Nach der Freigabe am 2026-11-02 eingearbeitet. Die Texte der Akzeptanzkriterien 10. **9.2:** F11 (Q1, CI-Drosselungstest mit Extra-Container) und F12 (Q2) ergänzt; 9.3 aktualisiert; 9.4 um zwei KTO-11-Hinweise aus dem Spike ergänzt. 11. **F12 entschieden (Chris, 2026-11-02):** Neuer Domänenfehler `AuthFailure.emailSendLimited` für `over_email_send_rate_limit` bei `signUp` und `resetPasswordForEmail`; `RegisterController` und `ForgotPasswordController` behandeln ihn wie Erfolg → neutraler CheckEmailScreen (4.3, 4.4, 5.4). CheckEmailScreen zeigt immer den Zusatz „Die E-Mail kann einige Minuten dauern …“, weil Nutzer eine Verzögerung sonst nicht erfahren. 7.1 Krit. 4 um Unit- und Widget-Tests ergänzt. R3/R6: Kanal in der App verdeckt, auf API-Ebene akzeptiert. Die Aussage „Krit. 4 gilt während einer Mail-Sperre nicht in der App“ ist entfallen. 12. **F12 ergänzt (Chris, 2026-11-02):** Auch „E-Mail erneut senden“ auf dem CheckEmailScreen bestätigt `over_email_send_rate_limit` neutral; im LoginScreen nach `email_not_confirmed` (Krit. 6) bleibt der Drosselungshinweis. `ResendController` mit `ResendOrigin.checkEmail | login` (4.4, 5.4); Tests in 7.1 Krit. 6; R3, R6, F12 angepasst. -13. **AUTH-CFG-25** neu: `auth.email.secure_password_change = false`, Entscheidung in KTO-11 (N13); 9.4 verweist darauf. **ID-Abgleich** mit dem B2-Stand (`supabase/config.toml`, `docs/auth-konfiguration.md`): IDs 01–24 stimmen überein. Angeglichen: AUTH-CFG-03 CLI-Schlüssel `auth.email.enabled` (statt `auth.email.enable_signup`). In B2 noch nachzuziehen: `secure_password_change` dort „ohne eigene ID“ → AUTH-CFG-25. +13. **AUTH-CFG-25** neu: `auth.email.secure_password_change = false`, Entscheidung in KTO-11 (N13); 9.4 verweist darauf. **ID-Abgleich** mit dem B2-Stand (`supabase/config.toml`, `docs/auth-konfiguration.md`): IDs 01–24 stimmen überein. Angeglichen: AUTH-CFG-03 CLI-Schlüssel `auth.email.enabled` (statt `auth.email.enable_signup`). Nachgezogen in B2: `secure_password_change` → AUTH-CFG-25 (erledigt). diff --git a/supabase/config.toml b/supabase/config.toml index e5f164c..984ed68 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -275,13 +275,13 @@ otp_expiry = 3600 # Betreff und Text verzweigen gleich (Spike B0 (a)). Staging/Prod: Vorlagen per URL # (GOTRUE_MAILER_TEMPLATES_CONFIRMATION, GOTRUE_MAILER_SUBJECTS_CONFIRMATION). [auth.email.template.confirmation] -subject = '{{ if and .Data (eq (index .Data "locale") "de") }}Bestätige deine E-Mail-Adresse für Shuffle{{ else }}Confirm your email address for Shuffle{{ end }}' +subject = '{{ if and .Data (eq (printf "%v" (index .Data "locale")) "de") }}Bestätige deine E-Mail-Adresse für Shuffle{{ else }}Confirm your email address for Shuffle{{ end }}' content_path = "./supabase/templates/confirmation.html" # AUTH-CFG-18 (wirkt lokal): Mailvorlage Passwort zurücksetzen, wie oben. # Staging/Prod: GOTRUE_MAILER_TEMPLATES_RECOVERY, GOTRUE_MAILER_SUBJECTS_RECOVERY. [auth.email.template.recovery] -subject = '{{ if and .Data (eq (index .Data "locale") "de") }}Passwort für Shuffle zurücksetzen{{ else }}Reset your Shuffle password{{ end }}' +subject = '{{ if and .Data (eq (printf "%v" (index .Data "locale")) "de") }}Passwort für Shuffle zurücksetzen{{ else }}Reset your Shuffle password{{ end }}' content_path = "./supabase/templates/recovery.html" # AUTH-CFG-19 (wirkt lokal): Benachrichtigungs-Mails explizit aus (KTO-01 Kriterium 11, nur Bestätigung diff --git a/supabase/migrations/20261102212208_auth_registration_rules.sql b/supabase/migrations/20261102212208_auth_registration_rules.sql index 2f8749c..bb9a200 100644 --- a/supabase/migrations/20261102212208_auth_registration_rules.sql +++ b/supabase/migrations/20261102212208_auth_registration_rules.sql @@ -2,19 +2,43 @@ -- 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'. +-- nach trim_ws (Dart-kompatibel). 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). +-- Trim wie Dart String.trim(): btrim() entfernt nur U+0020, Tab, Zeilenumbruch, NBSP usw. blieben +-- stehen. Explizite Liste (Unicode White_Space + BOM, identisch zur Dart-Doku): +-- U+0009-000D, 0020, 0085, 00A0, 1680, 2000-200A, 2028, 2029, 202F, 205F, 3000, FEFF. +-- Bewusst kein [[:space:]] (locale-abhängig). U+200B (Zero Width Space) ist kein Leerraum und +-- bleibt erlaubt, wie in Dart. +-- immutable + strict (NULL -> NULL), search_path leer, kein security definer. Execute bleibt für +-- public: Der Check-Constraint unten wird mit den Rechten der schreibenden Rolle ausgewertet +-- (authenticated beim Profil-Update); die Funktion ist rein und greift auf keine Daten zu. +create or replace function public.trim_ws(value text) +returns text +language sql +immutable +strict +parallel safe +set search_path = '' +as $$ + select pg_catalog.regexp_replace( + value, + '^[\u0009-\u000D\u0020\u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+' + || '|' || + '[\u0009-\u000D\u0020\u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+$', + '', + 'g' + ); +$$; + -- 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. +-- Der Constraint wird direkt (ohne NOT VALID) angelegt: Er prüft sofort den Bestand und bricht die +-- Migration ab, falls ein vorhandener (Entwicklungs-)Name nur aus Leerraum 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; + check (char_length(public.trim_ws(display_name)) between 1 and 30); -- 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 @@ -38,8 +62,10 @@ 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'); + name text := case when jsonb_typeof(meta -> 'display_name') = 'string' + then public.trim_ws(meta ->> 'display_name') end; + version text := case when jsonb_typeof(meta -> 'terms_version') = 'string' + then public.trim_ws(meta ->> 'terms_version') end; begin if coalesce(meta ->> 'age_confirmed', '') <> 'true' then raise exception 'age_confirmation_required'; diff --git a/supabase/templates/confirmation.html b/supabase/templates/confirmation.html index 539fc14..22d3c9e 100644 --- a/supabase/templates/confirmation.html +++ b/supabase/templates/confirmation.html @@ -3,13 +3,15 @@ AUTH-CFG-17: Bestätigungs-Mail (KTO-01 Kriterium 1, 11). Sprache aus user_metadata.locale ("de"), sonst Englisch. Keine externen Ressourcen, keine Bilder. Der Link führt auf die App-Seite /auth/confirm; erst der Klick dort löst die Prüfung aus (Spec E3). - Das Token (TokenHash) wird bewusst nicht als Klartext angezeigt, nur der Link. + Gültigkeit "1 Stunde" im Text entspricht AUTH-CFG-07 (GOTRUE_MAILER_OTP_EXP=3600); bei Änderung hier anpassen. + Der Link (inkl. TokenHash) ist sichtbar, damit er kopiert werden kann; der Code {{ .Token }} wird bewusst nicht angezeigt. */ -}} -{{ if and .Data (eq (index .Data "locale") "de") }} +{{ if and .Data (eq (printf "%v" (index .Data "locale")) "de") }} + Bestätige deine E-Mail-Adresse für Shuffle @@ -31,6 +33,7 @@ + Confirm your email address for Shuffle diff --git a/supabase/templates/recovery.html b/supabase/templates/recovery.html index a081f59..7b551c9 100644 --- a/supabase/templates/recovery.html +++ b/supabase/templates/recovery.html @@ -4,7 +4,7 @@ Sprache aus user_metadata.locale ("de"), sonst Englisch. Keine externen Ressourcen, keine Bilder. Der Link führt auf die App-Seite /auth/confirm; erst der Klick dort löst die Prüfung aus (Spec E3). */ -}} -{{ if and .Data (eq (index .Data "locale") "de") }} +{{ if and .Data (eq (printf "%v" (index .Data "locale")) "de") }} diff --git a/supabase/tests/database/05_auth_registration.test.sql b/supabase/tests/database/05_auth_registration.test.sql index e2ab005..8eb58de 100644 --- a/supabase/tests/database/05_auth_registration.test.sql +++ b/supabase/tests/database/05_auth_registration.test.sql @@ -1,7 +1,7 @@ -- KTO-01 Registrierung (Datenbankseite): Regeln des Triggers handle_new_user, Anzeigename, -- Nachweis der Zustimmung (terms_version), Sprache und Mailsprache-Sync. begin; -select plan(46); +select plan(60); -- --------------------------------------------------------------------------------------------- -- KTO-01 Kriterium 1: gültige Registrierung legt ein Profil an @@ -22,6 +22,17 @@ select ok((select terms_accepted_at is not null and age_confirmed_at is not null from public.profiles where id = '11111111-1111-1111-1111-111111111111'), 'KTO-01 Kriterium 1: Zeitpunkte von Zustimmung und Altersbestätigung sind gesetzt'); +-- Der Client sendet JSON-Booleans (kein String): beide Formen müssen gelten. +select lives_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values + ('88888888-8888-8888-8888-888888888888', 'bool@example.com', + '{"age_confirmed":true,"terms_accepted":true,"terms_version":"v1","display_name":"Bool"}')$$, + 'KTO-01 Kriterium 1: JSON-Booleans true werden angenommen'); +select throws_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'k2c@example.com', + '{"age_confirmed":false,"terms_accepted":true,"terms_version":"v1","display_name":"X"}')$$, + 'P0001', 'age_confirmation_required', 'KTO-01 Kriterium 2: JSON-Boolean age_confirmed=false wird abgelehnt'); + -- --------------------------------------------------------------------------------------------- -- KTO-01 Kriterium 2: Mindestalter muss bestätigt sein -- --------------------------------------------------------------------------------------------- @@ -61,6 +72,14 @@ select throws_ok( format($$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'tv4@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"%s","display_name":"X"}')$$, repeat('v', 51)), 'P0001', 'terms_version_required', 'QUE-04 Nachweis: terms_version mit 51 Zeichen wird abgelehnt'); +select throws_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'tv6@example.com', + '{"age_confirmed":"true","terms_accepted":"true","terms_version":"\u00a0","display_name":"X"}')$$, + 'P0001', 'terms_version_required', 'QUE-04 Nachweis: terms_version nur aus NBSP wird abgelehnt'); +select throws_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'tv7@example.com', + '{"age_confirmed":"true","terms_accepted":"true","terms_version":5,"display_name":"X"}')$$, + 'P0001', 'terms_version_required', 'QUE-04 Nachweis: terms_version als Zahl wird abgelehnt'); select lives_ok( format($$insert into auth.users (id, email, raw_user_meta_data) values ('55555555-5555-5555-5555-555555555555', 'tv5@example.com', @@ -96,7 +115,23 @@ select throws_ok( format($$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'n6@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"v1","display_name":"%s"}')$$, repeat('😀', 31)), 'P0001', 'display_name_invalid', 'KTO-01 Kriterium 10: 31 Emoji (Codepoints) werden abgelehnt'); -select is((select count(*) from auth.users where email ~ '^n[0-9]@example.com$'), 0::bigint, +select throws_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'n9@example.com', + '{"age_confirmed":"true","terms_accepted":"true","terms_version":"v1","display_name":"\t"}')$$, + 'P0001', 'display_name_invalid', 'KTO-01 Kriterium 10: Anzeigename nur aus Tab wird abgelehnt'); +select throws_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'n10@example.com', + '{"age_confirmed":"true","terms_accepted":"true","terms_version":"v1","display_name":"\n"}')$$, + 'P0001', 'display_name_invalid', 'KTO-01 Kriterium 10: Anzeigename nur aus Zeilenumbruch wird abgelehnt'); +select throws_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'n11@example.com', + '{"age_confirmed":"true","terms_accepted":"true","terms_version":"v1","display_name":"\u00a0"}')$$, + 'P0001', 'display_name_invalid', 'KTO-01 Kriterium 10: Anzeigename nur aus NBSP wird abgelehnt'); +select throws_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values (gen_random_uuid(), 'n12@example.com', + '{"age_confirmed":"true","terms_accepted":"true","terms_version":"v1","display_name":5}')$$, + 'P0001', 'display_name_invalid', 'KTO-01 Kriterium 10: Anzeigename als Zahl wird abgelehnt'); +select is((select count(*) from auth.users where email ~ '^n[0-9]+@example.com$'), 0::bigint, 'KTO-01 Kriterium 10: abgelehnte Registrierungen legen kein Konto an'); select lives_ok( format($$insert into auth.users (id, email, raw_user_meta_data) values @@ -111,11 +146,25 @@ select lives_ok( select is((select display_name from public.profiles where id = '77777777-7777-7777-7777-777777777777'), repeat('b', 30), 'KTO-01 Kriterium 10: der Name wird ohne Leerraum gespeichert'); +select lives_ok( + $$insert into auth.users (id, email, raw_user_meta_data) values + ('99999999-9999-9999-9999-999999999999', 'n13@example.com', + '{"age_confirmed":"true","terms_accepted":"true","terms_version":"v1","display_name":"Anna\t"}')$$, + 'KTO-01 Kriterium 10: Anzeigename mit Tab am Ende wird angenommen'); +select is((select display_name from public.profiles where id = '99999999-9999-9999-9999-999999999999'), + 'Anna', 'KTO-01 Kriterium 10: Tab wird entfernt, gespeichert wird Anna'); + -- Check-Constraint (gilt auch für spätere Änderungen, KTO-02) -select col_has_check('public', 'profiles', 'display_name', 'KTO-01 Kriterium 10: Anzeigename hat Check-Constraints'); +select ok(exists (select 1 from pg_constraint + where conrelid = 'public.profiles'::regclass and conname = 'profiles_display_name_not_blank' + and contype = 'c' and convalidated), + 'KTO-01 Kriterium 10: Constraint profiles_display_name_not_blank existiert und ist validiert'); select throws_ok( $$update public.profiles set display_name = ' ' where id = '11111111-1111-1111-1111-111111111111'$$, '23514', null, 'KTO-01 Kriterium 10: Constraint lehnt Anzeigename nur aus Leerzeichen ab (Update)'); +select throws_ok( + $$update public.profiles set display_name = chr(160) where id = '11111111-1111-1111-1111-111111111111'$$, + '23514', null, 'KTO-02 Kriterium 1: Constraint lehnt Anzeigename nur aus NBSP ab (Update)'); -- --------------------------------------------------------------------------------------------- -- KTO-01 Kriterium 11: Sprache @@ -179,5 +228,20 @@ select is((select raw_user_meta_data ->> 'display_name' from auth.users where id select is((select raw_user_meta_data ->> 'locale' from auth.users where id = '22222222-2222-2222-2222-222222222222'), null, 'KTO-01 Kriterium 11: Metadaten fremder Konten bleiben unverändert'); +-- anon darf die Trigger-Funktionen nicht ausführen +set local role anon; +select throws_ok($$select public.handle_new_user()$$, + '42501', null, 'KTO-01 Sicherheit: anon darf handle_new_user nicht ausführen'); +select throws_ok($$select public.sync_profile_locale_to_auth()$$, + '42501', null, 'KTO-01 Sicherheit: anon darf sync_profile_locale_to_auth nicht ausführen'); +reset role; + +-- Update auf denselben Wert löst keinen Sync aus (Cara: locale en, Marker in den Metadaten) +update auth.users set raw_user_meta_data = raw_user_meta_data || '{"locale":"xx"}' + where id = '33333333-3333-3333-3333-333333333333'; +update public.profiles set locale = 'en' where id = '33333333-3333-3333-3333-333333333333'; +select is((select raw_user_meta_data ->> 'locale' from auth.users where id = '33333333-3333-3333-3333-333333333333'), + 'xx', 'KTO-01 Kriterium 11: Update auf denselben Wert löst den Sync nicht aus'); + select * from finish(); rollback;