docs(auth): Spec für Anmelde-Einstieg und App Links (Issue #49) #55
2 changed files with 368 additions and 3 deletions
|
|
@ -22,7 +22,7 @@ Gleichzeitig ist #1 die **erste Story mit App-Code**. Das Flutter-Gerüst existi
|
||||||
| E1 | **App-Fundament als eigenes Chore-Issue #31** vor #1, nicht als Teil von #1 (entschieden, F10) | KAT-01/KAT-02 (R1.1) brauchen das Gerüst auch ohne Anmeldung; kleinere, prüfbare PRs; CLAUDE.md verlangt ein Issue je Änderung. `datenmodell-r1.md` Schritt 4 bündelt Gerüst und Anmeldung. Wir weichen davon nur im Zuschnitt ab, die Reihenfolge bleibt gleich. |
|
| E1 | **App-Fundament als eigenes Chore-Issue #31** vor #1, nicht als Teil von #1 (entschieden, F10) | KAT-01/KAT-02 (R1.1) brauchen das Gerüst auch ohne Anmeldung; kleinere, prüfbare PRs; CLAUDE.md verlangt ein Issue je Änderung. `datenmodell-r1.md` Schritt 4 bündelt Gerüst und Anmeldung. Wir weichen davon nur im Zuschnitt ab, die Reihenfolge bleibt gleich. |
|
||||||
| E2 | **Ein Issue #1, drei PRs** (01a Registrierung, 01b Anmelden, 01c Passwort zurücksetzen), davor ein Backend-PR (Migration + Auth-Konfiguration) | Die Kriterien teilen Backend-Konfiguration und Domänenschicht. Getrennte Issues würden Abhängigkeiten nur verschieben. Die PRs referenzieren `Refs #1`, der letzte `Closes #1`. **Abweichung vom Issue-Vorschlag:** Krit. 6 liegt in 01b statt 01a, weil der Hinweis auf die ausstehende Bestätigung beim *Anmelden* erscheint und den LoginScreen braucht. Die Aktion „E-Mail erneut senden“ entsteht schon in 01a (CheckEmailScreen) und wird in 01b wiederverwendet. |
|
| E2 | **Ein Issue #1, drei PRs** (01a Registrierung, 01b Anmelden, 01c Passwort zurücksetzen), davor ein Backend-PR (Migration + Auth-Konfiguration) | Die Kriterien teilen Backend-Konfiguration und Domänenschicht. Getrennte Issues würden Abhängigkeiten nur verschieben. Die PRs referenzieren `Refs #1`, der letzte `Closes #1`. **Abweichung vom Issue-Vorschlag:** Krit. 6 liegt in 01b statt 01a, weil der Hinweis auf die ausstehende Bestätigung beim *Anmelden* erscheint und den LoginScreen braucht. Die Aktion „E-Mail erneut senden“ entsteht schon in 01a (CheckEmailScreen) und wird in 01b wiederverwendet. |
|
||||||
| E3 | **Links in Mails zeigen auf eine App-Seite `/auth/confirm?token_hash=…&type=…`**, nicht auf `{{ .ConfirmationURL }}`. Die Seite löst `verifyOTP` erst per Knopfdruck aus. | Funktioniert geräteübergreifend: kein PKCE-Verifier nötig, z. B. Registrierung am Handy und Mail am PC. Mail-Scanner (z. B. Safe Links) verbrauchen den Einmal-Link nicht. Die Einmaligkeit bleibt serverseitig erhalten (Kriterium 8). |
|
| E3 | **Links in Mails zeigen auf eine App-Seite `/auth/confirm?token_hash=…&type=…`**, nicht auf `{{ .ConfirmationURL }}`. Die Seite löst `verifyOTP` erst per Knopfdruck aus. | Funktioniert geräteübergreifend: kein PKCE-Verifier nötig, z. B. Registrierung am Handy und Mail am PC. Mail-Scanner (z. B. Safe Links) verbrauchen den Einmal-Link nicht. Die Einmaligkeit bleibt serverseitig erhalten (Kriterium 8). |
|
||||||
| E4 | **In R1 landen Mail-Links immer in der Web-App.** Universal Links/App Links (iOS/Android) sind nicht Teil von #1. | Die Web-App kann alle Schritte ausführen. Universal Links brauchen AASA/assetlinks-Hosting und Store-Signaturen; das ist eine eigene Aufgabe und sollte ein Folge-Issue werden. |
|
| E4 | *(Abgelöst durch Spec #49 E2.)* **In R1 landen Mail-Links immer in der Web-App.** Universal Links/App Links (iOS/Android) sind nicht Teil von #1. | Die Web-App kann alle Schritte ausführen. Universal Links brauchen AASA/assetlinks-Hosting und Store-Signaturen; das ist eine eigene Aufgabe und sollte ein Folge-Issue werden. |
|
||||||
| E5 | **Mailsprache über `user_metadata.locale`**: Je Mailtyp gibt es *eine* Vorlage mit Go-Template-Verzweigung de/en (Standard Englisch, F6). Ein neuer DB-Trigger hält `auth.users.raw_user_meta_data.locale` synchron zu `profiles.locale`. | GoTrue kennt nur eine Vorlage je Typ und stellt `.Data` (= `user_metadata`) bereit. Die Profilsprache ist maßgeblich (Kriterium 11, KTO-11 Kriterium 2). Ein Send-Email-Hook (Edge Function + eigener SMTP-Versand) wäre mehr bewegliche Teile ohne zweiten Anwendungsfall. Spike B0 hat bestätigt, dass Betreff und Text `.Data.locale` auswerten (Spike-Dokument (a)); ein Rückfall ist nicht nötig. |
|
| E5 | **Mailsprache über `user_metadata.locale`**: Je Mailtyp gibt es *eine* Vorlage mit Go-Template-Verzweigung de/en (Standard Englisch, F6). Ein neuer DB-Trigger hält `auth.users.raw_user_meta_data.locale` synchron zu `profiles.locale`. | GoTrue kennt nur eine Vorlage je Typ und stellt `.Data` (= `user_metadata`) bereit. Die Profilsprache ist maßgeblich (Kriterium 11, KTO-11 Kriterium 2). Ein Send-Email-Hook (Edge Function + eigener SMTP-Versand) wäre mehr bewegliche Teile ohne zweiten Anwendungsfall. Spike B0 hat bestätigt, dass Betreff und Text `.Data.locale` auswerten (Spike-Dokument (a)); ein Rückfall ist nicht nötig. |
|
||||||
| E6 | **Validierung zweistufig**: Feldfehler im Client (Kriterien 2, 3, 10). Der Server erzwingt dieselben Regeln als Schutz (Trigger, `minimum_password_length = 10`). | Der Client gibt sofort Rückmeldung am Feld. Der Server vertraut nie dem Client. GoTrue meldet Trigger-Fehler als HTTP 500; der Body enthält zwar die Meldung des Triggers (z. B. `display_name_invalid`, Spike A4; gilt auch für U+200B im Anzeigenamen), das Format ist aber nicht vertraglich zugesichert. Der Client bleibt deshalb die nutzersichtbare Stelle. |
|
| E6 | **Validierung zweistufig**: Feldfehler im Client (Kriterien 2, 3, 10). Der Server erzwingt dieselben Regeln als Schutz (Trigger, `minimum_password_length = 10`). | Der Client gibt sofort Rückmeldung am Feld. Der Server vertraut nie dem Client. GoTrue meldet Trigger-Fehler als HTTP 500; der Body enthält zwar die Meldung des Triggers (z. B. `display_name_invalid`, Spike A4; gilt auch für U+200B im Anzeigenamen), das Format ist aber nicht vertraglich zugesichert. Der Client bleibt deshalb die nutzersichtbare Stelle. |
|
||||||
| E7 | **„Liste häufiger Passwörter“** wird nur im Client als gebündelte Liste geprüft; der Server erzwingt die Mindestlänge 10. **Kein Have I Been Pwned** (entschieden, F2): `GOTRUE_PASSWORD_HIBP_ENABLED=false`. | HIBP würde einen SHA-1-Präfix an einen US-Dienst (Cloudflare/HIBP) übertragen (Klärung 21/47). Außerdem gibt es HIBP in der lokalen Supabase CLI nicht. Restrisiko: Über die rohe API lassen sich häufige Passwörter mit mindestens 10 Zeichen setzen (R14). |
|
| E7 | **„Liste häufiger Passwörter“** wird nur im Client als gebündelte Liste geprüft; der Server erzwingt die Mindestlänge 10. **Kein Have I Been Pwned** (entschieden, F2): `GOTRUE_PASSWORD_HIBP_ENABLED=false`. | HIBP würde einen SHA-1-Präfix an einen US-Dienst (Cloudflare/HIBP) übertragen (Klärung 21/47). Außerdem gibt es HIBP in der lokalen Supabase CLI nicht. Restrisiko: Über die rohe API lassen sich häufige Passwörter mit mindestens 10 Zeichen setzen (R14). |
|
||||||
|
|
@ -328,8 +328,8 @@ Alle Pakete unterstützen iOS, Android und Web. `flutter-dev` pinnt die Versione
|
||||||
| Thema | Web | iOS / Android |
|
| Thema | Web | iOS / Android |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Sitzung (Krit. 5) | `localStorage`; bleibt nach Reload und Browser-Neustart erhalten. Kein Abgleich zwischen Tabs: ein zweiter Tab merkt die Abmeldung erst beim nächsten Refresh. | SharedPreferences (App-Sandbox); bleibt nach Neustart erhalten. Bei Neuinstallation ist die Sitzung weg. |
|
| Sitzung (Krit. 5) | `localStorage`; bleibt nach Reload und Browser-Neustart erhalten. Kein Abgleich zwischen Tabs: ein zweiter Tab merkt die Abmeldung erst beim nächsten Refresh. | SharedPreferences (App-Sandbox); bleibt nach Neustart erhalten. Bei Neuinstallation ist die Sitzung weg. |
|
||||||
| Mail-Links (E3, E4) | `/auth/confirm` braucht `usePathUrlStrategy` **und** eine SPA-Rewrite-Regel im Webserver (alle Pfade → `index.html`, Aufgabe D1). | Links öffnen in R1 den Browser (Web-App). Universal/App Links kommen in einem Folge-Issue. |
|
| Mail-Links (E3, E4; E4 abgelöst durch Spec #49 E2) | `/auth/confirm` braucht `usePathUrlStrategy` **und** eine SPA-Rewrite-Regel im Webserver (alle Pfade → `index.html`, Aufgabe D1). | Links öffnen in R1 den Browser (Web-App). Universal/App Links kommen in einem Folge-Issue. |
|
||||||
| Autofill / Passwortmanager | `AutofillHints.email`, `newPassword`, `password`; Flutter Web unterstützt Passwortmanager nur eingeschränkt (bekannt, im PR vermerken) | Autofill iOS/Android inkl. „Starkes Passwort vorschlagen“; Associated Domains erst mit Universal Links |
|
| Autofill / Passwortmanager | `AutofillHints.email`, `newPassword`, `password`; Flutter Web unterstützt Passwortmanager nur eingeschränkt (bekannt, im PR vermerken) | Autofill iOS/Android inkl. „Starkes Passwort vorschlagen“; Associated Domains erst mit Universal Links (siehe Spec #49 F2) |
|
||||||
| Tastatur | Tab-Reihenfolge = Lesereihenfolge, Enter sendet ab, sichtbarer Fokus (QUE-06 Krit. 4) | `TextInputType.emailAddress`, `TextInputAction.next/done` |
|
| Tastatur | Tab-Reihenfolge = Lesereihenfolge, Enter sendet ab, sichtbarer Fokus (QUE-06 Krit. 4) | `TextInputType.emailAddress`, `TextInputAction.next/done` |
|
||||||
| Offline | Login/Registrierung/Reset brauchen Verbindung → Hinweis statt technischem Fehler (Querschnitt Zustände 2). Kein Connectivity-Paket; der Fehler wird beim Aufruf erkannt. | gleich |
|
| Offline | Login/Registrierung/Reset brauchen Verbindung → Hinweis statt technischem Fehler (Querschnitt Zustände 2). Kein Connectivity-Paket; der Fehler wird beim Aufruf erkannt. | gleich |
|
||||||
| Layout (Plattformen Krit. 2) | ab 600 dp zentrierte Karte, max. 480 dp breit | Vollbild, scrollbar (200 % Schrift) |
|
| Layout (Plattformen Krit. 2) | ab 600 dp zentrierte Karte, max. 480 dp breit | Vollbild, scrollbar (200 % Schrift) |
|
||||||
|
|
@ -533,3 +533,7 @@ Nach der Freigabe am 2026-11-02 eingearbeitet. Die Texte der Akzeptanzkriterien
|
||||||
### 2026-11-03 – A11y-Fix Semantics-Split (Issue #41, Branch `fix/41-semantics-split`)
|
### 2026-11-03 – A11y-Fix Semantics-Split (Issue #41, Branch `fix/41-semantics-split`)
|
||||||
|
|
||||||
1. **Ursache und Fix:** Die `Card` im Layout ab 600 dp faltete Titel, Text und Knopf in einen Semantics-Knoten. `AppFormScaffold` setzt `Semantics(explicitChildNodes: true)` um den Karteninhalt (Regel in `docs/design/auth-screens.md`). Der Befund aus T3 (A11y-Befund QUE-06) ist damit behoben; `pressFirstButton` ist entfernt.
|
1. **Ursache und Fix:** Die `Card` im Layout ab 600 dp faltete Titel, Text und Knopf in einen Semantics-Knoten. `AppFormScaffold` setzt `Semantics(explicitChildNodes: true)` um den Karteninhalt (Regel in `docs/design/auth-screens.md`). Der Befund aus T3 (A11y-Befund QUE-06) ist damit behoben; `pressFirstButton` ist entfernt.
|
||||||
|
|
||||||
|
### 2026-11-03 – Verweis auf Spec #49 (Issue #49)
|
||||||
|
|
||||||
|
E4 und § 6 (Mail-Links, Autofill) sind durch Spec #49 E2/F2 abgelöst: Universal Links (iOS) und App Links (Android) für `/auth/confirm` werden dort spezifiziert. Der Inhalt dieser Spec ist sonst unverändert.
|
||||||
|
|
|
||||||
361
docs/specs/49-mobil-anmeldung-app-links.md
Normal file
361
docs/specs/49-mobil-anmeldung-app-links.md
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
# Spec #49 – Mobil: Einstieg in die Anmeldung und Rückweg aus E-Mail-Links (iOS/Android)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Issue | #49 (`type/tech`, `prio/should`, Milestone R1), Refs #1 |
|
||||||
|
| Status | Entwurf, wartet auf Freigabe durch Chris |
|
||||||
|
| Autor | architect |
|
||||||
|
| Stand | 2026-11-03 |
|
||||||
|
| Abhängigkeit | KTO-01 (#1) umgesetzt; berührt QUE-09 (#29) Kriterium 13 und KTO-11 (#28); Betrieb nach ADR-0002 (Aufgabe D1 aus Spec #1, noch ohne eigenes Issue) |
|
||||||
|
| Quellen | Issue #49, `docs/specs/1-auth-registrierung-anmeldung.md` (E3, E4, § 5.3, § 5.4, § 6), `docs/auth-konfiguration.md` (AUTH-CFG-01, -02, § 3), `app/lib/core/routing/`, `app/lib/features/auth/presentation/providers/auth_redirect.dart`, `requirements/20-r1-akzeptanzkriterien.md` § QUE-09, § KTO-11, `requirements/r1-klaerungen.md` N9, ADR-0001, ADR-0002, Quelltext `supabase_flutter` 2.x (`SupabaseAuth`) und `app_links` 7.2.2 (iOS-Plugin) |
|
||||||
|
|
||||||
|
## 1. Ziel
|
||||||
|
|
||||||
|
Auf iOS und Android sollen Nutzer (a) aus der App heraus zur Anmeldung und Registrierung kommen und (b) mit den Links aus der Bestätigungs- und der Reset-Mail **in der App** landen, wenn sie installiert ist. Ohne App und auf anderen Geräten bleibt die Web-App der Zielort; der geräteübergreifende Ablauf aus Spec #1 E3 bleibt erhalten. Spec #1 E4 („Mail-Links landen in R1 immer in der Web-App“) wird durch diese Spec abgelöst (E2; Verweis in Spec #1 über Aufgabe W2). Das Verhalten im Web ändert sich bei den Mail-Links nicht.
|
||||||
|
|
||||||
|
**Abweichung vom Vorschlag im Issue:** Das Issue schlägt Universal/App Links für `/auth/confirm` **und** `/reset-password` vor. Diese Spec beansprucht nur `/auth/confirm` (E2), weil `/reset-password` nie Ziel eines Mail-Links ist.
|
||||||
|
|
||||||
|
Befund (Issue #49, iOS-Simulator, main @ 455b3bf):
|
||||||
|
|
||||||
|
1. Die öffentliche Startseite (`HomeScreen`) hat keinen Weg zu `/login` oder `/register`. `privateRoutePaths` ist leer, deshalb leitet auch kein Guard dorthin. Auf Mobilgeräten sind die Auth-Screens dadurch nicht erreichbar.
|
||||||
|
2. Die Mail-Links zeigen auf `<SITE_URL>/auth/confirm?token_hash=…&type=email|recovery`. Die App beansprucht diese URLs nicht, deshalb öffnet der Link den Browser. Lokal zeigt `SITE_URL` auf `http://localhost:3000`, wo im Simulator keine Web-App läuft; dort lässt sich die Bestätigung gar nicht abschließen. In Produktion würde der Link in der mobilen Web-App funktionieren, der Nutzer wäre dann aber im Browser angemeldet und nicht in der App.
|
||||||
|
|
||||||
|
### 1.1 Optionen
|
||||||
|
|
||||||
|
**Einstieg in die Anmeldung**
|
||||||
|
|
||||||
|
| Option | Beschreibung | Vorteile | Nachteile |
|
||||||
|
|---|---|---|---|
|
||||||
|
| E-a | **Hinweis-Karte auf der Startseite** (abgemeldet): kurzer Text, „Anmelden“, „Konto erstellen“ | Entspricht QUE-09 Kriterium 13 („Hinweis mit dem Weg zum Anmelden und Registrieren“, N9); QUE-09 kann die Karte übernehmen; klein | Nur auf der Startseite sichtbar (bisher die einzige öffentliche Seite) |
|
||||||
|
| E-b | Aktion „Anmelden“ in der AppBar | Von jeder Seite mit AppBar erreichbar | Es gibt noch keine gemeinsame AppBar bzw. Shell; das wäre eine Shell-Entscheidung vor QUE-09/KTO-11; schwächeres Touch-Ziel und Text-Platz bei 200 % Schrift |
|
||||||
|
| E-c | Navigationsleiste mit Tab „Konto“ | Späterer Ort für KTO-11 | Vorgriff auf Navigation und Einstellungen ohne Inhalt; größerer Eingriff |
|
||||||
|
| E-d | Private Startseite (Guard leitet abgemeldet nach `/login`) | Kein UI nötig | Widerspricht QUE-09 Kriterium 13 und Sicherheit Kriterium 2 (Katalog öffentlich), Web-Startseite wäre nicht mehr öffentlich |
|
||||||
|
|
||||||
|
**Rückweg aus den Mail-Links**
|
||||||
|
|
||||||
|
| Option | Beschreibung | Vorteile | Nachteile |
|
||||||
|
|---|---|---|---|
|
||||||
|
| L-a | **Universal Links (iOS) und App Links (Android) auf der bestehenden https-Adresse `<SITE_URL>/auth/confirm`** | Ein Link für alle Geräte; ohne App öffnet derselbe Link die Web-App (Fallback ohne Zusatzaufwand); Mailvorlagen, GoTrue und Web bleiben unverändert; kein Fremdanbieter; Links sind an Domain, Team-ID und Signatur gebunden; eine fremde App kann sie nach erfolgreicher Verifizierung nicht abfangen (Android: verlässlich erst ab Android 12, siehe R13) | Braucht Apple-Developer-Konto (Team-ID), Play-Signaturschlüssel, endgültige Domain und IDs sowie öffentlich erreichbare Zuordnungsdateien; lokal kaum Ende-zu-Ende testbar; manche Mail-Programme lösen Universal Links voraussichtlich nicht aus (z. B. Outlook Safe Links, In-App-Browser) |
|
||||||
|
| L-b | Eigenes URL-Schema (`shuffle://auth/confirm?…`) direkt in der Mail | Lokal sofort testbar, keine Konten nötig | **Abgelehnt.** Ohne App öffnet der Link nichts, das widerspricht dem Fallback im Web (Kriterium 4) und dem geräteübergreifenden Ablauf (E3, Registrierung am Handy, Mail am PC); viele Mail-Programme machen Nicht-http-Links nicht klickbar; jede App kann dasselbe Schema registrieren und den `token_hash` abfangen, beim Reset-Link wäre das eine Kontoübernahme |
|
||||||
|
| L-c | Eigenes Schema als `redirect_to` über `{{ .ConfirmationURL }}` (GoTrue `/verify` leitet zur App weiter) | Supabase-Standardweg | **Abgelehnt.** Hebt E3 auf: Der Link würde beim ersten Aufruf eingelöst, Mail-Scanner verbrauchen ihn; geräteübergreifend nur mit PKCE-Verifier; Abfang-Risiko wie L-b; Allow-List müsste das Schema aufnehmen |
|
||||||
|
| L-d | Code statt Link (6-stelliges OTP `{{ .Token }}` in der Mail, Eingabe in der App) | Keine Domain-Dateien, lokal testbar | Widerspricht KTO-01 Kriterium 8 („Link“); neue UX und neue Mailvorlagen; 6 Ziffern sind ohne Drosselung je Konto ratbar (Drosselung nur je IP, AUTH-CFG-12) |
|
||||||
|
| L-e | Status quo: Link öffnet immer die Web-App, danach in der App anmelden | Kein Aufwand, funktioniert in Produktion | Erfüllt Kriterien 2 und 3 nicht; lokal im Simulator nicht abschließbar |
|
||||||
|
|
||||||
|
### 1.2 Empfehlung und Kernentscheidungen
|
||||||
|
|
||||||
|
| # | Entscheidung (Vorschlag) | Begründung |
|
||||||
|
|---|---|---|
|
||||||
|
| E1 | **Einstieg über eine Hinweis-Karte auf der Startseite (E-a)**, auf allen Plattformen inklusive Web, nur im Status `signedOut`. Die Karte ist ein eigenes Widget `SignInPrompt` in `features/auth/presentation/widgets/`, das QUE-09 später auf der neuen Startseite wiederverwendet. Keine AppBar-Aktion, keine Navigationsleiste. | Deckt Kriterium 1 und vorab den Anmelde-Teil von QUE-09 Kriterium 13 ab. Eine Shell oder Navigationsleiste ist eine eigene Entscheidung mit QUE-09/KTO-11. Gleiches Verhalten auf allen Plattformen ist einfacher als eine Weiche nur für Mobile. |
|
||||||
|
| E2 | **Universal Links und App Links (L-a), beschränkt auf genau den Pfad `/auth/confirm`.** `/reset-password` gehört **nicht** dazu: Es ist nie Ziel eines Mail-Links, die App erreicht es per Redirect nach `verifyOTP` (Spec #1 § 5.3). Alle anderen Web-Adressen öffnen weiter den Browser. | Kleinster nötiger Umfang. Spätere öffentliche Web-Adressen (Katalog, Spieldetail) sollen nicht ungeplant in der App landen. Eine Erweiterung ist eine eigene Entscheidung (F1). KTO-11 (E-Mail-Wechsel, `type=email_change`) nutzt denselben Pfad und profitiert bei der **Link-Zuordnung** ohne Änderung. `EmailLinkType.tryParse` (`app/lib/features/auth/domain/email_link_type.dart`) kennt heute nur `email`, `signup` und `recovery`; `email_change` ergäbe „ungültig“. `EmailLinkType` und die Mailvorlage ergänzt KTO-11 (#28), Hinweis dort. |
|
||||||
|
| E3 | **Kein eigenes URL-Schema**, weder in Mails noch als Fallback (L-b, L-c abgelehnt). | Sicherheit (Abfangen des Reset-Tokens), Fallback ohne App, E3 aus Spec #1. |
|
||||||
|
| E4 | **Keine Backend-Änderung.** Keine Migration, keine Edge Function, Mailvorlagen und `GOTRUE_URI_ALLOW_LIST` (AUTH-CFG-02) bleiben unverändert. Einzige Bedingung: Der Host von `SITE_URL` (AUTH-CFG-01) ist genau die Domain, die die Zuordnungsdateien ausliefert und in den App-Projekten eingetragen ist. | Die Vorlagen bauen den Link aus `{{ .SiteURL }}` und `{{ .TokenHash }}`, nicht aus GoTrue `/verify?redirect_to=…`. Die Allow-List greift dabei nicht. |
|
||||||
|
| E5 | **Deep Links über das eingebaute Flutter-Deep-Linking an go_router**, kein zusätzliches Paket. `FlutterDeepLinkingEnabled` (iOS) und `flutter_deeplinking_enabled` (Android) werden ausdrücklich auf `true` gesetzt. `EmailLinkScreen` bleibt unverändert der Zielort, auch der Knopfdruck vor `verifyOTP` (Spec #1 E3) bleibt. | go_router verarbeitet Pfad und Query bereits (Route `/auth/confirm` existiert). Belegt im Quelltext (Review, 2026-11-03): `supabase_flutter` 2.18.3 hört über `app_links` (transitiv vorhanden) mit, reagiert aber nur auf `access_token`, `code`, `error*` (`SupabaseAuth._defaultIsAuthCallbackDeeplink`) und ignoriert unsere Links mit `token_hash`; das iOS-Plugin von `app_links` 7.2.2 gibt Links standardmäßig an andere Empfänger weiter (`defaultUrlHandling = .never`); die Flutter-Engine 3.47.9 verarbeitet Deep Links selbst (iOS: `FlutterDeepLinkingEnabled` ohne Eintrag `YES`; Android: `onNewIntent` pusht die Route); go_router loggt Routen nur mit `debugLogDiagnostics`. Das Zusammenspiel am Gerät bzw. Simulator (UIScene, Kalt- und Warmstart) prüft S1. |
|
||||||
|
| E6 | **Ein Issue, zwei PRs:** PR A „Einstieg“ (sofort umsetzbar, Kriterium 1), PR B „App Links“ (Kriterien 2–4). PR B ist für Android und die Vorlagen ohne Chris-Aufgaben mergebar (Dateien mit Platzhaltern, geprüft durch Tests). **Einschränkung iOS:** Das Xcode-Projekt nutzt bereits `DEVELOPMENT_TEAM = KSX6GJY9C5`. Ob das ein bezahltes Team ist, klärt C1. Bis dahin legt PR B `Runner.entitlements` an, verdrahtet `CODE_SIGN_ENTITLEMENTS` aber noch nicht (oder nur über eine xcconfig-Weiche, die standardmäßig aus ist), damit Gerätebuilds mit einem kostenlosen Personal Team nicht brechen. Die CI baut nur Web und würde den Bruch nicht bemerken. Die Gerätebestätigung der Kriterien 2 und 3 hängt an C1–C6 (§ 8.3). | PR A soll nicht auf Konten und Domain warten; ein iOS-Gerätebuild darf nicht unbemerkt brechen. |
|
||||||
|
| E7 | **Zuordnungsdateien liegen als Vorlage im Repo** (Team-ID und Zertifikat-Fingerabdrücke sind nicht geheim); ein Test prüft sie gegen Bundle-ID, Application-ID, Pfad und Host. Wo sie ausgeliefert werden (Web-Build oder Gateway), klärt S1. | Repo und Betrieb bleiben deckungsgleich (ADR-0002: Einstellungen im Repo festhalten). |
|
||||||
|
| E8 | **Host je Plattform an genau einer Stelle konfiguriert** (Android: Gradle-Property, iOS: xcconfig-Variable), Standard ist die Produktions-Domain (Platzhalter `shuffle.cshomelabs.work`, F8 aus Spec #1). | Domain ist noch Platzhalter; eine Änderung soll eine Zeile je Plattform sein. Staging-Builds sind offen (F5). |
|
||||||
|
|
||||||
|
## 2. Akzeptanzkriterien
|
||||||
|
|
||||||
|
Übernommen aus Issue #49 (Entwurf). Referenz in Tests und PRs: „#49 Kriterium n“.
|
||||||
|
|
||||||
|
1. Auf iOS und Android ist die Anmeldung aus der App heraus erreichbar.
|
||||||
|
2. Der Bestätigungslink aus der Mail öffnet die App und schließt die Registrierung ab.
|
||||||
|
3. Der Passwort-Reset-Link öffnet die App und führt in den Reset-Ablauf.
|
||||||
|
4. Verhalten ohne installierte App (Fallback im Web) ist beschrieben.
|
||||||
|
|
||||||
|
**Vorschläge zur Präzisierung (nicht entschieden, Freigabe durch Chris, siehe F10):**
|
||||||
|
|
||||||
|
- Zu 1: Bin ich abgemeldet, zeigt die Startseite einen Hinweis mit „Anmelden“ und „Konto erstellen“, die zu `/login` bzw. `/register` führen. Das gilt auch im Web. Bin ich angemeldet, erscheint der Hinweis nicht.
|
||||||
|
- Zu 2: Gegeben die App ist installiert und ich tippe auf dem Gerät auf den Bestätigungslink, dann öffnet sich die Bestätigungsseite in der App; nach dem Knopfdruck „E-Mail-Adresse bestätigen“ bin ich **in der App** angemeldet. Das gilt bei kaltem Start und bei laufender App.
|
||||||
|
- Zu 3: wie 2 für den Reset-Link; nach dem Knopfdruck führt die App zu „Neues Passwort“, danach gelten KTO-01 Kriterien 8 und 9 unverändert.
|
||||||
|
- Zu 4 (testbar formuliert): Gegeben die App ist nicht installiert, ich öffne den Link an einem anderen Gerät, oder das Mail-Programm übergibt den Link nicht an die App, dann öffnet sich die Web-App und schließt Bestätigung bzw. Reset ab wie bisher (Spec #1 § 5.4); nach einer Bestätigung im mobilen Browser sehe ich den bestehenden Hinweis „Du kannst dich jetzt auch in der App anmelden.“
|
||||||
|
- Zusätzlich 5: Im Web ändert sich das Verhalten der Mail-Links nicht (die bestehenden Playwright-Tests `web/*` bleiben unverändert grün).
|
||||||
|
|
||||||
|
**Relevante Querschnittskriterien:** QUE-05 Kriterien 2 und 4 (neue Texte in ARB, de/en); QUE-06 Kriterien 1–4 (Karte: Beschriftung, Kontrast AA, Touch-Ziele ≥ 48 dp, 200 % Schrift, Tastatur im Web); Sicherheit der Daten Kriterium 2 (Startseite bleibt öffentlich, keine privaten Daten); Plattformen Kriterien 1 und 2.
|
||||||
|
|
||||||
|
## 3. Datenmodell
|
||||||
|
|
||||||
|
Keine Änderung. Keine neuen Tabellen, Spalten, Policies oder pgTAP-Tests.
|
||||||
|
|
||||||
|
**RLS:** unverändert. Der Deep Link überträgt nur `token_hash` und `type`; eingelöst wird er wie bisher per `verifyOTP` gegen GoTrue.
|
||||||
|
|
||||||
|
## 4. Backend
|
||||||
|
|
||||||
|
### 4.1 Supabase / GoTrue
|
||||||
|
|
||||||
|
| Punkt | Wert | Änderung |
|
||||||
|
|---|---|---|
|
||||||
|
| Mailvorlagen (AUTH-CFG-17, -18) | Link `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email` bzw. `…&type=recovery` | **keine** |
|
||||||
|
| `GOTRUE_SITE_URL` (AUTH-CFG-01) | Prod: `https://<Produktions-Domain>` | Bedingung: Host = Associated Domain (APPLINK-01). Auf Prod setzt das Chris (C5). |
|
||||||
|
| `GOTRUE_URI_ALLOW_LIST` (AUTH-CFG-02) | `<SITE_URL>/auth/confirm` | **keine** (kein `redirect_to` im Einsatz) |
|
||||||
|
| lokal `config.toml` | `site_url = "http://localhost:3000"` | **keine**; lokal gibt es keine Universal Links (siehe 6.2) |
|
||||||
|
| RPCs / Edge Functions | – | keine |
|
||||||
|
|
||||||
|
### 4.2 Neue Konfigurations-IDs (Abschnitt in `docs/auth-konfiguration.md`)
|
||||||
|
|
||||||
|
Neuer Abschnitt „App Links / Universal Links“ mit eigenen IDs, damit Tests und Release-Checkliste darauf verweisen. Die bestehenden AUTH-CFG-IDs bleiben unverändert.
|
||||||
|
|
||||||
|
| ID | Einstellung | Wert | Ort |
|
||||||
|
|---|---|---|---|
|
||||||
|
| APPLINK-01 | Associated Domain | Host von `SITE_URL` (Prod), Platzhalter `shuffle.cshomelabs.work` | Android: Gradle-Property `shuffleAppLinkHost` (Standard in `android/gradle.properties`), über `manifestPlaceholders["appLinkHost"]`; iOS: `APP_LINK_HOST` in `ios/Flutter/AppLinks.xcconfig`, eingebunden in `Debug.xcconfig` und `Release.xcconfig` |
|
||||||
|
| APPLINK-02 | Beanspruchter Pfad | genau `/auth/confirm` (mit beliebiger Query) | Android-Intent-Filter, AASA `components`, assetlinks (Android verifiziert je Host, der Pfad steht im Intent-Filter) |
|
||||||
|
| APPLINK-03 | iOS-App-ID | `<TEAM_ID>.work.cshomelabs.shuffle` (Bundle-ID Platzhalter, R15 aus Spec #1). Im Xcode-Projekt steht bereits `DEVELOPMENT_TEAM = KSX6GJY9C5`; bestätigt Chris es als bezahltes Team (C1), wird `<TEAM_ID>` durch `KSX6GJY9C5` ersetzt | `apple-app-site-association` |
|
||||||
|
| APPLINK-04 | Android-Paket und Signatur | `work.cshomelabs.shuffle`, SHA-256 des Upload-Schlüssels **und** des Play-App-Signing-Schlüssels | `assetlinks.json` |
|
||||||
|
| APPLINK-05 | Auslieferung | `https://<Domain>/.well-known/apple-app-site-association` und `/.well-known/assetlinks.json`: HTTPS mit gültigem Zertifikat, Status 200 **ohne Weiterleitung**, `Content-Type: application/json`, öffentlich erreichbar (Apple lädt über sein CDN), **vom SPA-Rewrite ausgenommen** | Gateway bzw. Web-Hosting (D1, C5) |
|
||||||
|
| APPLINK-06 | Flutter-Deep-Linking | `FlutterDeepLinkingEnabled = true` (Info.plist), `flutter_deeplinking_enabled = true` (Manifest) | App-Projekte |
|
||||||
|
|
||||||
|
### 4.3 Zuordnungsdateien (Vorlagen)
|
||||||
|
|
||||||
|
Ablageort laut S1: bevorzugt `app/web/.well-known/` (wenn `flutter build web` versteckte Verzeichnisse mitkopiert und das Hosting sie korrekt ausliefert), sonst `deploy/well-known/` mit Auslieferung am Gateway. Platzhalter werden im Repo durch die echten Werte ersetzt, sobald C1–C3 erledigt sind.
|
||||||
|
|
||||||
|
`apple-app-site-association` (ohne Dateiendung):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"applinks": {
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"appIDs": ["<TEAM_ID>.work.cshomelabs.shuffle"],
|
||||||
|
"components": [
|
||||||
|
{ "/": "/auth/confirm", "comment": "E-Mail-Links (Bestätigung, Reset, später E-Mail-Wechsel)" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"webcredentials": {
|
||||||
|
"apps": ["<TEAM_ID>.work.cshomelabs.shuffle"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`assetlinks.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "work.cshomelabs.shuffle",
|
||||||
|
"sha256_cert_fingerprints": ["<UPLOAD_KEY_SHA256>", "<PLAY_APP_SIGNING_SHA256>"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
`webcredentials` (iOS) und `delegate_permission/common.get_login_creds` (Android, optional) verbinden die Passwortspeicher von App und Web. Ob sie aufgenommen werden, entscheidet F2 (Empfehlung: `webcredentials` ja).
|
||||||
|
|
||||||
|
### 4.4 Fehlerfälle
|
||||||
|
|
||||||
|
Keine neuen Server-Fehler. Alle Fehler beim Einlösen laufen wie bisher über das Mapping aus Spec #1 § 4.4 (`linkInvalidOrExpired`, `network`, `rateLimited`, `unexpected`). Ein Link einer anderen Umgebung (z. B. Staging-Link in der Prod-App) ergibt beim Einlösen `linkInvalidOrExpired`; das ist harmlos.
|
||||||
|
|
||||||
|
## 5. Frontend
|
||||||
|
|
||||||
|
### 5.1 PR A: Einstieg (Kriterium 1)
|
||||||
|
|
||||||
|
**Neues Widget `SignInPrompt`** (`app/lib/features/auth/presentation/widgets/sign_in_prompt.dart`):
|
||||||
|
|
||||||
|
- Inhalt: kurze Überschrift und ein Satz (Vorschlag ui-designer, z. B. „Melde dich an, um deine Sammlung und Partien zu verwalten.“), Primärknopf „Anmelden“ → `context.go(AppRoutes.login)`, Sekundärknopf „Konto erstellen“ → `context.go(AppRoutes.register)`. Kein `from`-Parameter (Ziel nach dem Login ist ohnehin `/`).
|
||||||
|
- Komponenten aus `core/widgets` (`AppPrimaryButton`, für den Sekundärknopf gegebenenfalls eine neue Variante durch ui-designer). Touch-Ziele ≥ 48 dp, Semantics über die Knopf-Labels, Überschrift als `Semantics(header: true)`.
|
||||||
|
- Dumm: bekommt keine Logik, nur Navigation per Callback oder `context.go`.
|
||||||
|
|
||||||
|
**`HomeScreen`** wird `ConsumerWidget`:
|
||||||
|
|
||||||
|
- Liest **nur** `showSignInPromptProvider` (siehe unten), nicht `authStatusProvider` direkt. Der Provider bildet die Zustände so ab:
|
||||||
|
- `signedOut` → Platzhaltertext und darunter `SignInPrompt`.
|
||||||
|
- `signedIn` → Platzhaltertext wie bisher, **kein** Prompt (keine Abmelden-Funktion, siehe F4).
|
||||||
|
- `passwordRecovery` → tritt auf `/` nicht auf (Redirect nach `/reset-password`).
|
||||||
|
- Laden (Stream ohne ersten Wert) → Platzhaltertext ohne Prompt, damit beim Start mit gespeicherter Sitzung der Prompt nicht kurz aufblinkt. Der Wert kommt praktisch sofort (`initialSession`).
|
||||||
|
- Fehler im Stream → wie `signedOut` behandeln (Prompt anzeigen), Fehler nur ins Log.
|
||||||
|
- Die Entscheidung „Prompt ja/nein“ trifft ein kleiner Provider `showSignInPromptProvider` (bool) in `features/auth/presentation/providers/`, der `authStatusProvider` liest; das Widget entscheidet nichts. Damit bleibt der HomeScreen ohne Logik und QUE-09 übernimmt Provider und Widget.
|
||||||
|
- **Import über Feature-Grenzen (`home` → `auth/presentation`):** bewusst. Der Anmelde-Hinweis gehört fachlich zu `auth` (Routen, Status); `home` ist nur Gastgeber. Die Abhängigkeit geht in eine Richtung (`auth` kennt `home` nicht), und QUE-09 ersetzt `home` später durch die Startseite, die denselben Import nutzt. Ein Verschieben nach `core/widgets` lohnt erst mit einem zweiten Gastgeber.
|
||||||
|
- Breite Bildschirme: Karte max. 480 dp breit, zentriert (wie `AppFormScaffold`).
|
||||||
|
|
||||||
|
**Texte (ARB, de/en):** `signInPromptTitle`, `signInPromptBody`, `signInPromptSignIn`, `signInPromptRegister`. Bestehende Schlüssel für „Anmelden“/„Konto erstellen“ dürfen wiederverwendet werden, wenn der Wortlaut passt.
|
||||||
|
|
||||||
|
### 5.2 PR B: App Links (Kriterien 2–4)
|
||||||
|
|
||||||
|
**Keine neue Route, kein neuer Screen.** Der Deep Link landet auf der bestehenden Route `/auth/confirm` (`EmailLinkScreen` mit `token_hash` und `type`).
|
||||||
|
|
||||||
|
Abläufe in der App:
|
||||||
|
|
||||||
|
| Situation | Verhalten |
|
||||||
|
|---|---|
|
||||||
|
| Kaltstart über den Link | Flutter übergibt `/auth/confirm?token_hash=…&type=…` als Startroute; erwartet: go_router nutzt sie statt `initialLocation` (zu bestätigen in S1 (a); sonst `overridePlatformDefaultLocation` prüfen). Redirect-Regeln wie bisher: `signedOut`/`signedIn` → bleibt; `passwordRecovery` → `/reset-password`. |
|
||||||
|
| App läuft (warm) | Flutter meldet die neue Route (Android: `onNewIntent` pusht sie, belegt); erwartet: go_router ersetzt den Stack (`go`), zu bestätigen in S1 (f). Nicht gespeicherte Eingaben auf dem vorherigen Screen gehen dann verloren (R10). |
|
||||||
|
| Bestätigung erfolgreich | Hinweis „E-Mail bestätigt“, Knopf „Weiter“ → `/`. Nutzer ist in der App angemeldet, die Startseite zeigt keinen Prompt mehr. Der Zusatz „auch in der App anmelden“ erscheint in der App nicht (`isMobileWebProvider` ist dort `false`). |
|
||||||
|
| Reset erfolgreich | Status `passwordRecovery` → `/reset-password` (Spec #1 § 5.4, 5.5 unverändert, Guard nur im Prozess). |
|
||||||
|
| Link ungültig/abgelaufen | Fehlerzustand mit „Neuen Link anfordern“ wie bisher. |
|
||||||
|
| Zurück-Taste (Android) auf `/auth/confirm` nach Kaltstart | Kein Eintrag darunter: Die App schließt sich. Akzeptiert; „Weiter“ führt zur Startseite. |
|
||||||
|
| Link öffnet, während ein anderes Konto angemeldet ist | `verifyOTP` ersetzt die Sitzung, wie heute im Web (F3). |
|
||||||
|
|
||||||
|
**Plattform-Dateien** (Umsetzung flutter-dev):
|
||||||
|
|
||||||
|
- `android/app/src/main/AndroidManifest.xml`: in `MainActivity` zusätzlicher Intent-Filter mit `android:autoVerify="true"`, `action VIEW`, `category DEFAULT` und `BROWSABLE`, `data android:scheme="https" android:host="${appLinkHost}" android:path="/auth/confirm"`. Dazu `meta-data flutter_deeplinking_enabled = true`. `launchMode="singleTop"` bleibt (warme Links kommen über `onNewIntent`).
|
||||||
|
- `android/app/build.gradle.kts`: `manifestPlaceholders["appLinkHost"]` aus der Gradle-Property `shuffleAppLinkHost` (APPLINK-01).
|
||||||
|
- `ios/Runner/Runner.entitlements` (neu): `com.apple.developer.associated-domains` = `applinks:$(APP_LINK_HOST)` und (F2) `webcredentials:$(APP_LINK_HOST)`. **Verdrahtung (E6):** `CODE_SIGN_ENTITLEMENTS` wird erst gesetzt, wenn C1 bestätigt, dass `KSX6GJY9C5` ein bezahltes Team ist. Bis dahin bleibt die Einstellung leer, oder sie kommt über eine xcconfig-Variable (z. B. `SHUFFLE_ENTITLEMENTS` in `ios/Flutter/AppLinks.xcconfig`, Standard leer), die nach C1 auf `Runner/Runner.entitlements` gesetzt wird. Für Tests auf Geräten im Entwicklermodus optional `applinks:$(APP_LINK_HOST)?mode=developer` (umgeht den Apple-CDN-Cache), **nur in Debug**. Mechanismus: der Suffix kommt aus einer Variablen `APP_LINK_MODE_SUFFIX`, die nur `Debug.xcconfig` auf `?mode=developer` setzt (`Release.xcconfig` lässt sie leer); der Eintrag lautet dann `applinks:$(APP_LINK_HOST)$(APP_LINK_MODE_SUFFIX)`. Alternative: eigene `Runner-Debug.entitlements` nur für Debug.
|
||||||
|
- `ios/Runner/Info.plist`: `FlutterDeepLinkingEnabled = true`.
|
||||||
|
- `ios/Runner/SceneDelegate.swift`: unverändert, sofern S1 zeigt, dass `FlutterSceneDelegate` Universal Links (`scene(_:willConnectTo:options:)` und `scene(_:continue:)`) an die Engine weitergibt.
|
||||||
|
|
||||||
|
**Logging:** `token_hash` darf in keinem Log erscheinen (Spec #1: keine personenbezogenen bzw. geheimen Daten). Zu prüfen: go_router `debugLogDiagnostics` bleibt aus, `AppLogger` und der globale Fehler-Handler loggen keine vollständigen Routen-URIs, `EmailLinkController` loggt nur Typ und Ergebnis. `supabase_flutter` loggt den URI nur für Links, die es selbst verarbeitet (nicht unsere).
|
||||||
|
|
||||||
|
### 5.3 Domänenmodelle, Provider
|
||||||
|
|
||||||
|
- Neu: `showSignInPromptProvider` (bool aus `authStatusProvider`, Regeln aus 5.1).
|
||||||
|
- Unverändert: `AuthRepository`, `AuthStatus`, `EmailLinkController`, Redirect-Regeln (`authRedirectFor`), `privateRoutePaths` (bleibt leer).
|
||||||
|
|
||||||
|
### 5.4 Neue Pakete
|
||||||
|
|
||||||
|
**Keine.** `app_links` ist über `supabase_flutter` bereits transitiv vorhanden (7.2.2, Apache-2.0, 160/160 Punkte, pub.dev geprüft 2026-11-03) und wird **nicht** direkt genutzt. Rückfall, falls S1 zeigt, dass Flutters eingebautes Deep-Linking mit dem `app_links`-Plugin kollidiert: `FlutterDeepLinkingEnabled=false` setzen, `app_links` als direkte Abhängigkeit aufnehmen und dessen `uriLinkStream` in einem kleinen Adapter in `core/routing` an `router.go()` weitergeben. Diese Variante bräuchte eine kurze Ergänzung der Spec.
|
||||||
|
|
||||||
|
## 6. Plattform-Besonderheiten
|
||||||
|
|
||||||
|
### 6.1 Übersicht
|
||||||
|
|
||||||
|
| Thema | iOS | Android | Web |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Einstieg (Krit. 1) | Hinweis-Karte auf `/` | Hinweis-Karte auf `/` | Hinweis-Karte auf `/` (auch hier, QUE-09 Krit. 13); URLs `/login`, `/register` weiter direkt aufrufbar |
|
||||||
|
| Link-Zuordnung | Universal Links: Entitlement `applinks:<Host>`, AASA wird bei Installation und regelmäßig über Apples CDN geladen | App Links: Intent-Filter mit `autoVerify`, `assetlinks.json` wird bei Installation geprüft (Android 12+: ohne erfolgreiche Prüfung öffnet der Browser) | keine Änderung; `/auth/confirm` wie bisher (SPA-Rewrite, D1) |
|
||||||
|
| Voraussetzungen (Konten) | Bezahltes Apple-Developer-Programm (Team-ID, Capability „Associated Domains“; mit einem kostenlosen Personal Team nach unserem Kenntnisstand nicht möglich, C1 bestätigt) | Play Console und Play App Signing (Fingerabdruck), eigener Upload-Schlüssel statt Debug-Signatur (heute signiert `release` mit dem Debug-Schlüssel) | – |
|
||||||
|
| Wann öffnet die App nicht? | Link in Safari-Adresszeile eingegeben; Link über Weiterleitungsdienste (voraussichtlich Outlook Safe Links, Tracking-Redirects); manche In-App-Browser von Mail-Apps; Nutzer hat „In Safari öffnen“ gewählt (iOS merkt sich das je Domain) | App-Link-Prüfung fehlgeschlagen; Nutzer hat in den Einstellungen „Unterstützte Links öffnen“ deaktiviert; manche In-App-Browser | – |
|
||||||
|
| Android-Versionen | – | `minSdk = flutter.minSdkVersion`, also auch Android 7–11. Dort zeigt Android bei fehlender oder fehlgeschlagener Verifizierung einen Auswahldialog; eine andere App mit passendem Intent-Filter kann den Link dann empfangen. Verlässlicher Abfangschutz erst ab Android 12 mit erfolgreicher Verifizierung (R13) | – |
|
||||||
|
| Fallback | Web-App öffnet `/auth/confirm` und schließt den Schritt ab; der Nutzer ist dann im Browser angemeldet, nicht in der App; Hinweis „auch in der App anmelden“ (bestehend) | gleich | ist der Normalfall |
|
||||||
|
| Debug-Builds | Kaltstart einer Debug-Build per Link funktioniert nicht ohne Flutter-Tooling (bekannte Flutter-Einschränkung; Quelle: Hinweis im iOS-Plugin von `app_links` 7.2.2 mit Verweis auf flutter/flutter#149214 und llfbandit/app_links#211); Gerätetests mit Profile/Release | Debug funktioniert, solange der Debug-Fingerabdruck in `assetlinks.json` steht (nur Staging, F5) | – |
|
||||||
|
| Kamera, Dateien, Push, Offline | nicht betroffen; Einlösen braucht Netz (Offline-Hinweis aus Spec #1) | gleich | gleich |
|
||||||
|
|
||||||
|
### 6.2 Lokale Entwicklung
|
||||||
|
|
||||||
|
- Lokal (`site_url = http://localhost:3000`) gibt es keine Universal/App Links. Die Mails in Mailpit zeigen weiter auf `localhost:3000`, dort funktioniert der Web-Ablauf.
|
||||||
|
- **Android lokal prüfbar:** Den `token_hash` aus der Mailpit-Mail nehmen und mit dem App-Link-Host aufrufen: `adb shell am start -W -a android.intent.action.VIEW -d "https://<APP_LINK_HOST>/auth/confirm?token_hash=<…>&type=email" work.cshomelabs.shuffle`. Mit expliziter Paketangabe startet die App auch ohne erfolgreiche Verifizierung; sie löst den Token gegen das lokale Supabase ein (Env `dev.json`). Verifizierungsstatus: `adb shell pm get-app-links work.cshomelabs.shuffle`.
|
||||||
|
- **iOS lokal:** Ohne Team-ID und erreichbare AASA nicht Ende-zu-Ende prüfbar. Die App-Logik deckt ein Widget-Test mit simulierter Plattform-Route ab (7.1). Ende-zu-Ende erst auf Staging/Prod mit echter Domain (C6).
|
||||||
|
|
||||||
|
## 7. Teststrategie
|
||||||
|
|
||||||
|
Werkzeuge: `flutter_test` + `mocktail` (Unit/Widget), bestehende Playwright-Tests (`e2e/`, Projekt `web`) als Regression, ein Konfigurations-Test für die Plattform-Dateien, manuelle Gerätematrix.
|
||||||
|
|
||||||
|
### 7.1 Abdeckungsmatrix
|
||||||
|
|
||||||
|
| AK | Unit | Widget | Integration/E2E | RLS | Task |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| #49 Krit. 1 | `showSignInPromptProvider`: `signedOut` → true, `signedIn` → false, Laden → false, Fehler → true | HomeScreen abgemeldet: Prompt mit zwei Knöpfen sichtbar; Tipp „Anmelden“ → LoginScreen, „Konto erstellen“ → RegisterScreen; angemeldet: kein Prompt; a11y-Guidelines (`labeledTapTargetGuideline`, `androidTapTargetGuideline`, `iOSTapTargetGuideline`, `textContrastGuideline`), `textScaler` 2.0 ohne Overflow, de/en | Playwright `web`: Startseite abgemeldet zeigt den Prompt, Klick führt zu `/login` und `/register` (Web gehört dazu, E1); **QUE-06 Kriterium 4:** per Tab erreichbar in Lesereihenfolge (Überschrift/Text → „Anmelden“ → „Konto erstellen“), sichtbarer Fokus, Enter bzw. Leertaste löst den fokussierten Knopf aus | – | U1, A1, T1 |
|
||||||
|
| #49 Krit. 2 | – | Kaltstart: `tester.binding.platformDispatcher.defaultRouteNameTestValue = '/auth/confirm?token_hash=x&type=email'` → `EmailLinkScreen` mit Typ Bestätigung; warm: `tester.binding.handlePushRoute(...)` bzw. `SystemChannels.navigation` `pushRouteInformation` → `EmailLinkScreen`; Knopfdruck ruft `verifyEmailLink` (Mock) genau einmal; Erfolg → „Weiter“ → `/`; die Prüfung „Prompt verschwunden“ setzt PR A voraus (T2 hängt deshalb von A1 ab, § 8.2) | manuell M1–M4 (7.2) | – | A2, T2 |
|
||||||
|
| #49 Krit. 3 | – | wie Krit. 2 mit `type=recovery`: nach Erfolg und Status `passwordRecovery` → ResetPasswordScreen; Deep Link während `passwordRecovery` → bleibt auf `/reset-password` | manuell M5–M6 | – | A2, T2 |
|
||||||
|
| #49 Krit. 4 | – | – | Playwright `web` (bestehend, unverändert): kompletter Bestätigungs- und Reset-Ablauf im Browser; manuell M7–M9 (App nicht installiert, PC, Outlook) | – | T2 |
|
||||||
|
| Vorschlag 5 (Web unverändert) | bestehende Redirect-Tests (`authRedirectFor`) unverändert grün | – | alle Playwright-Projekte `api`, `web`, `throttle` unverändert grün | – | T2 |
|
||||||
|
| APPLINK-01 bis -06 | **Konfigurations-Test** `app/test/platform/app_links_config_test.dart` (liest Dateien relativ zu `app/`): Manifest enthält Intent-Filter mit `autoVerify="true"`, `scheme=https`, `host=${appLinkHost}`, `path=/auth/confirm` und `flutter_deeplinking_enabled=true`; Gradle-Property gesetzt; Entitlements-Datei existiert und enthält `applinks:$(APP_LINK_HOST)`; `CODE_SIGN_ENTITLEMENTS` vor C1 leer bzw. nur über die Weiche gesetzt, nach C1 in allen Build-Konfigurationen (der Test prüft den jeweils gültigen Stand); Info.plist `FlutterDeepLinkingEnabled=true`; AASA und assetlinks sind gültiges JSON, Bundle-/Package-ID stimmen mit Xcode-Projekt bzw. `applicationId` überein, Pfad ist genau `/auth/confirm`; xcconfig-Host = Gradle-Host | – | Release-Checkliste (7.3) | – | A2, T2 |
|
||||||
|
| Logging | Test: Router-Fehler- und Logpfade erhalten bei einem `/auth/confirm`-URI keinen `token_hash` (z. B. `AppLogger` mit Test-Handler, Route mit Token aufrufen, Logausgabe prüfen) | – | – | – | T2 |
|
||||||
|
|
||||||
|
### 7.2 Manuelle Gerätematrix (Profile- oder Release-Build, nach C1–C6)
|
||||||
|
|
||||||
|
Je Plattform (iOS-Gerät, Android-Gerät bzw. Emulator mit Play-Services) und jeweils kalt und warm:
|
||||||
|
|
||||||
|
| # | Fall | Erwartung |
|
||||||
|
|---|---|---|
|
||||||
|
| M1 | Bestätigungslink in Apple Mail (iOS) bzw. Gmail (Android) | App öffnet `/auth/confirm`, Knopf, angemeldet in der App |
|
||||||
|
| M2 | Bestätigungslink in Gmail-App (iOS) | App öffnet oder Fallback Web (dokumentieren, welches) |
|
||||||
|
| M3 | Bestätigungslink in Outlook (beide) | voraussichtlich Fallback Web wegen Safe Links (dokumentieren, welches) |
|
||||||
|
| M4 | App läuft im Hintergrund auf einer anderen Seite | App kommt nach vorn, `/auth/confirm` |
|
||||||
|
| M5 | Reset-Link | App, Knopf, „Neues Passwort“, Erfolg, andere Sitzung wird abgemeldet (KTO-01 Krit. 9) |
|
||||||
|
| M6 | Reset-Link zweimal benutzt | zweites Mal „Link abgelaufen oder schon benutzt“ |
|
||||||
|
| M7 | App nicht installiert | Web-App schließt den Schritt ab |
|
||||||
|
| M8 | Link am PC geöffnet, Registrierung war am Handy | Web-App am PC bestätigt, Anmeldung in der App danach möglich |
|
||||||
|
| M9 | iOS: Nutzer wählt „In Safari öffnen“ | Web-App; beim nächsten Link bleibt iOS beim Browser (bekanntes iOS-Verhalten, dokumentieren) |
|
||||||
|
| M10 | Android 7–11 (falls `minSdk` < 31 bleibt, F11) | App öffnet nach Verifizierung bzw. Auswahldialog; Verhalten dokumentieren |
|
||||||
|
|
||||||
|
Ergebnis als Tabelle im PR B bzw. im Issue #49; Abweichungen (z. B. M2, M3) werden beschrieben, nicht als Fehler gewertet, solange der Fallback greift.
|
||||||
|
|
||||||
|
### 7.3 Ergänzung Release-Checkliste (`docs/auth-konfiguration.md` § 5)
|
||||||
|
|
||||||
|
- [ ] `curl -sI https://<Domain>/.well-known/apple-app-site-association` und `/.well-known/assetlinks.json`: Status 200, keine Weiterleitung, `Content-Type: application/json`, Inhalt identisch zum Repo (APPLINK-05).
|
||||||
|
- [ ] Apple-CDN-Sicht: `https://app-site-association.cdn-apple.com/a/v1/<Domain>` liefert die aktuelle Datei.
|
||||||
|
- [ ] Google-Prüfung: Digital Asset Links API (`https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://<Domain>&relation=delegate_permission/common.handle_all_urls`) listet Paket und Fingerabdrücke.
|
||||||
|
- [ ] `GOTRUE_SITE_URL`-Host = APPLINK-01.
|
||||||
|
- [ ] Gerätematrix 7.2 auf der Release-Build durchgeführt.
|
||||||
|
|
||||||
|
## 8. Aufgaben
|
||||||
|
|
||||||
|
### 8.1 PR A – Einstieg (`feature/49-anmeldung-einstieg`, `Refs #49`)
|
||||||
|
|
||||||
|
| # | Agent | Aufgabe | abhängig von |
|
||||||
|
|---|---|---|---|
|
||||||
|
| U1 | ui-designer | Gestaltung `SignInPrompt` (Karte, Primär- und Sekundärknopf, Abstände, breite Bildschirme), Sekundärknopf-Variante in `core/widgets` falls nötig, Textvorschläge de/en, a11y (Überschrift als Header, Fokusreihenfolge Web) | – |
|
||||||
|
| A1 | flutter-dev | `SignInPrompt`, `showSignInPromptProvider`, `HomeScreen` als `ConsumerWidget` (5.1), ARB-Schlüssel de/en, `build_runner` | U1 |
|
||||||
|
| T1 | test-engineer | Unit-Test Provider, Widget-Tests HomeScreen/Prompt (7.1 Krit. 1), Playwright `web`: Prompt und Navigation | A1 |
|
||||||
|
|
||||||
|
### 8.2 PR B – App Links (`feature/49-app-links`, `Refs #49`; #49 wird erst nach der Gerätebestätigung geschlossen, siehe F7)
|
||||||
|
|
||||||
|
| # | Agent | Aufgabe | abhängig von |
|
||||||
|
|---|---|---|---|
|
||||||
|
| S1 | flutter-dev | **Spike (max. ½ Tag), Ergebnis als Änderungsprotokoll in dieser Spec.** Zu klären: (a) Flutter 3.47: Ist Deep-Linking ohne Flag aktiv, und übergibt die Engine Pfad **und** Query an go_router (Kaltstart, warm)? (b) iOS mit UIScene (`SceneDelegate.swift`, `FlutterSceneDelegate`): kommen `scene(_:willConnectTo:)` und `scene(_:continue:)` mit `NSUserActivityTypeBrowsingWeb` bei go_router an, während das `app_links`-Plugin registriert ist? (c) Android mit `singleTop`: `onNewIntent` erreicht go_router? (d) Verarbeitet `supabase_flutter` die Links tatsächlich nicht (kein `getSessionFromUrl`)? (e) Kopiert `flutter build web` `web/.well-known/` in `build/web/`, und liefert `serve -s` bzw. das geplante Hosting die AASA mit `application/json` aus? (f) Ersetzt go_router bei warmem Link den Stack (`go`) oder legt er ihn oben drauf? Prüfweg: Android-Emulator per `adb … am start` (6.2), iOS-Simulator mit `xcrun simctl openurl` **mit einem eigenen Test-Schema nur im lokalen Spike-Branch** (nicht committen), um den Weg bis go_router zu sehen; Universal-Link-Zuordnung selbst ist ohne C1 nicht prüfbar. | – |
|
||||||
|
| A2 | flutter-dev | Plattform-Dateien nach 5.2 (Manifest-Intent-Filter, Gradle-Property, Entitlements, xcconfig, Info.plist-Flag; `CODE_SIGN_ENTITLEMENTS` erst nach C1 bzw. über die xcconfig-Weiche, E6); Logging-Prüfung (5.2); falls S1 negativ: Rückfall-Adapter nach 5.4 (vorher Spec-Ergänzung). **Pflicht vor dem PR (lokal, die CI baut nur Web):** `flutter build apk --release` und `flutter build ios --no-codesign`, wenn möglich zusätzlich ein Gerätebuild mit Signatur; Ergebnis im PR vermerken | S1 |
|
||||||
|
| D1a | devops | AASA- und assetlinks-Vorlagen nach 4.3 am Ort aus S1 (e); Doku-Abschnitt APPLINK-01 bis -06 in `docs/auth-konfiguration.md`; Release-Checkliste 7.3; Gateway-Vorgaben (Auslieferung ohne Redirect, `application/json`, `/.well-known/` vom SPA-Rewrite ausgenommen, öffentlich erreichbar, nicht hinter VPN/Basic-Auth) als Ergänzung zu D1 aus Spec #1; Android-Release-Signatur vorbereiten (Upload-Keystore außerhalb des Repos; prüfen, dass `key.properties`, `*.jks` und `*.keystore` weiter ignoriert werden, das steht bereits in `.gitignore` und `app/android/.gitignore`; Signing-Config in `build.gradle.kts`), ohne Schlüssel selbst anzulegen | S1 |
|
||||||
|
| T2 | test-engineer | Konfigurations-Test (7.1 APPLINK), Widget-Tests Deep Link kalt/warm (Krit. 2, 3), Logging-Test, Regression Playwright; lokale Builds wie bei A2 nachvollziehen; Vorlage der Gerätematrix 7.2 im PR | A2, D1a, A1 (PR A gemergt, wegen „Prompt verschwunden“ in 7.1 Krit. 2) |
|
||||||
|
| W1 | docs-writer | Outline-Seite KTO-01 bzw. neue Seite „App Links“ (Ablauf, Fallback, Grenzen) nach Merge | A2, D1a |
|
||||||
|
| W2 | architect / docs-writer | Ablösung in Spec #1 dokumentieren: § 10 Eintrag „E4 und § 6 (Mail-Links, Autofill) durch Spec #49 E2/F2 abgelöst“, Verweise an E4 (Zeile „E4“ in § 1) und § 6 (Zeilen „Mail-Links“ und „Autofill / Passwortmanager“), jeweils „Abgelöst durch Spec #49 E2“ bzw. „siehe Spec #49 F2“. Nur Verweise und Änderungsprotokoll, keine Inhaltsänderung. **Noch offen:** Im Review-Commit nicht enthalten, weil der Commit über die Forgejo-API läuft und Spec #1 dafür vollständig neu übertragen werden müsste (rund 50.000 Tokens, fehleranfällig). Erledigt der nächste Agent mit Shell-Zugriff auf diesem Branch, spätestens mit PR A. | – |
|
||||||
|
|
||||||
|
### 8.3 Aufgaben, die nur Chris erledigen kann
|
||||||
|
|
||||||
|
| # | Aufgabe | Warum Chris | blockiert |
|
||||||
|
|---|---|---|---|
|
||||||
|
| C1 | **Team bestätigen:** Das Xcode-Projekt nutzt bereits `DEVELOPMENT_TEAM = KSX6GJY9C5` (Debug, Release, Profile). Chris bestätigt, ob das ein bezahltes Apple-Developer-Program-Team ist. Wenn ja: `KSX6GJY9C5` wird in APPLINK-03 und der AASA-Vorlage eingetragen, „Associated Domains“ für die App-ID aktiviert und `CODE_SIGN_ENTITLEMENTS` verdrahtet (E6). Wenn nein (kostenloses Personal Team): Beitritt zum Programm (kostenpflichtig) oder iOS-Teil zurückstellen; bis dahin bleiben die Entitlements unverdrahtet | Vertrag, Zahlung, Identität | iOS-Teil von Krit. 2/3, M1–M9 iOS, Verdrahtung `CODE_SIGN_ENTITLEMENTS` |
|
||||||
|
| C2 | **Google Play Console** einrichten, Play App Signing aktivieren, SHA-256 des App-Signing-Schlüssels mitteilen; Upload-Keystore erzeugen und sicher verwahren (nicht im Repo), SHA-256 mitteilen | Konto, Schlüsselhoheit | Android-Teil von Krit. 2/3 in Release-Builds |
|
||||||
|
| C3 | **Bundle-ID / Application-ID endgültig festlegen** (heute Platzhalter `work.cshomelabs.shuffle`, R15 aus Spec #1) | Nach Store-Veröffentlichung nicht mehr änderbar; AASA und assetlinks hängen daran | C1, C2, finale Dateien |
|
||||||
|
| C4 | **Produktions-Domain endgültig festlegen** (heute Platzhalter `shuffle.cshomelabs.work`, F8 aus Spec #1) und sicherstellen, dass sie öffentlich per HTTPS erreichbar ist (kein VPN, keine Zugangssperre vor `/.well-known/`) | Domain und DNS gehören Chris | APPLINK-01, -05 |
|
||||||
|
| C5 | **Auf Prod:** Zuordnungsdateien ausliefern (Werte aus C1–C3), `GOTRUE_SITE_URL` auf diese Domain setzen, Release-Checkliste 7.3 abhaken | Zugriff auf Server und Prod-Konfiguration nur Chris (kein Agent greift auf Server zu) | Krit. 2/3 Ende-zu-Ende |
|
||||||
|
| C6 | Gerätematrix 7.2 auf echten Geräten durchführen oder abnehmen (iPhone, Android) | Physische Geräte, Apple-ID | Abschluss #49 |
|
||||||
|
| C7 | Offene Fragen F1–F11 entscheiden, Spec freigeben | Freigabe | Umsetzung |
|
||||||
|
|
||||||
|
### 8.4 Reihenfolge
|
||||||
|
|
||||||
|
```
|
||||||
|
sofort PR A: U1 → A1 → T1 (unabhängig von allem anderen)
|
||||||
|
parallel S1 (Spike) → A2, D1a → (PR A gemergt) → T2 → PR B (mergebar mit Platzhaltern, iOS-Entitlements unverdrahtet bis C1)
|
||||||
|
Chris C1 (Team bestätigen), C3, C4 → C2 → C5 → C6 → #49 schließen (F7)
|
||||||
|
danach W1; nach C1: Verdrahtung CODE_SIGN_ENTITLEMENTS (kleiner Folge-PR, flutter-dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
PR B ändert `HomeScreen` nicht. Die Umsetzung von A2 ist unabhängig von PR A; der Widget-Test zu Krit. 2 („Prompt verschwunden“) braucht PR A, deshalb wird PR B nach PR A gemergt. Kein Backend-PR nötig (backend-dev hat in #49 keine Aufgabe).
|
||||||
|
|
||||||
|
## 9. Risiken & offene Fragen
|
||||||
|
|
||||||
|
### 9.1 Risiken
|
||||||
|
|
||||||
|
| # | Risiko | Auswirkung | Gegenmaßnahme |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R1 | Mail-Programme und Weiterleitungsdienste (voraussichtlich Outlook Safe Links, Tracking-Redirects, In-App-Browser) lösen Universal/App Links nicht aus | Link öffnet die Web-App statt der App | Fallback Web ist vollwertig (Krit. 4); Gerätematrix dokumentiert das Verhalten; kein Tracking-Redirect in eigenen Mails (bereits so, Spec #1 § 4.1) |
|
||||||
|
| R2 | SPA-Rewrite liefert `index.html` statt der AASA aus, oder es gibt eine Weiterleitung (z. B. http→https, `www`) | Universal/App Links fallen still aus | APPLINK-05, Release-Checkliste 7.3 mit `curl`; `/.well-known/` ausdrücklich vom Rewrite ausnehmen (D1a) |
|
||||||
|
| R3 | Apple lädt die AASA über sein CDN und cacht sie (Stunden bis Tage); Android prüft `assetlinks.json` vor allem bei der Installation | Änderungen wirken verzögert; eine bei Installation fehlende Datei lässt die App-Link-Prüfung scheitern | Dateien **vor** der ersten Store-Installation ausliefern; iOS-Entwicklermodus (`?mode=developer`) für Tests; Android: `pm verify-app-links --re-verify` |
|
||||||
|
| R4 | Signatur passt nicht zu `assetlinks.json` (heute `release` mit Debug-Schlüssel, Play App Signing signiert neu) | App Links auf Android ohne Wirkung | Beide Fingerabdrücke eintragen (C2); Release-Signatur vorbereiten (D1a); Prüfung über die Digital Asset Links API |
|
||||||
|
| R5 | Bundle-/Application-ID oder Domain ändern sich nach Veröffentlichung | Dateien und Builds müssen neu, alte Installationen öffnen keine Links mehr | C3, C4 vor dem ersten Store-Build; Werte nur an einer Stelle (E8) |
|
||||||
|
| R6 | Ein späterer, breiterer Pfad-Umfang zieht Web-Adressen ungewollt in die App | Nutzer landen in der App, wo die Seite fehlt (404) | Genau `/auth/confirm` (E2); Erweiterung nur mit Entscheidung (F1) |
|
||||||
|
| R7 | `token_hash` gelangt in Logs (App-Logger, Router-Diagnose, Android-System-Log bei `am start`) | Einmal-Token lesbar, beim Reset Kontoübernahme bis zum Ablauf (1 h) | Logging-Test (7.1); `debugLogDiagnostics` aus; Token ist einmalig und läuft nach 1 h ab (AUTH-CFG-07); `adb`-Aufrufe nur lokal |
|
||||||
|
| R8 | Flutter-Deep-Linking und das transitive `app_links`-Plugin stören sich (iOS UIScene), oder eine künftige `supabase_flutter`-Version wertet `token_hash` selbst aus | Links kommen nicht bei go_router an oder werden doppelt eingelöst (E3 verletzt) | S1; Rückfall 5.4; bei Updates von `supabase_flutter` `_defaultIsAuthCallbackDeeplink` prüfen (Hinweis im PR). Als weitere Option bleibt `detectSessionInUri: false` in `Supabase.initialize`. Wir nutzen weder OAuth noch PKCE-Redirects, die Option wirkt aber auch im Web; sie wird deshalb nur mit eigener Prüfung eingesetzt. |
|
||||||
|
| R9 | Homelab-Domain (`cshomelabs.work`) ist nicht öffentlich oder hinter einer Zugangssperre | Apple-CDN und Google erreichen die Dateien nicht | C4; APPLINK-05 |
|
||||||
|
| R10 | Warmer Deep Link ersetzt den Navigationsstack | Nicht gespeicherte Eingaben auf dem vorherigen Screen gehen verloren | Akzeptiert: Mail-Links sind ein bewusster Wechsel; S1 (f) bestätigt das Verhalten |
|
||||||
|
| R11 | iOS-Debug-Builds starten nicht kalt über einen Link | Fehlalarm beim Testen | Gerätetests mit Profile/Release (6.1) |
|
||||||
|
| R12 | Bestätigung im mobilen Browser (Fallback) meldet den Nutzer im Browser an, nicht in der App | Nutzer muss sich in der App zusätzlich anmelden | Bestehender Hinweis „Du kannst dich jetzt auch in der App anmelden.“; die Startseite der App bietet den Einstieg (Krit. 1) |
|
||||||
|
| R13 | **Abfangschutz nur eingeschränkt:** App Links schützen vor fremden Apps nur bei erfolgreicher Verifizierung und verlässlich erst ab Android 12. `minSdk = flutter.minSdkVersion` schließt Android 7–11 ein; dort kann bei fehlender Verifizierung eine andere App mit passendem Intent-Filter den Link (und damit den `token_hash`) empfangen. Auf iOS greift die Zuordnung nur nach erfolgreichem Laden der AASA | Bei einem Reset-Link wäre das eine Kontoübernahme bis zum Ablauf | E3 aus Spec #1 begrenzt den Schaden: Der Token wird erst per Knopfdruck eingelöst, ist einmalig und läuft nach 1 h ab (AUTH-CFG-07); eine neue Anforderung ersetzt ihn. Verifizierung in der Release-Checkliste (7.3). Option: `minSdk ≥ 31` (F11) |
|
||||||
|
| R14 | **Login-CSRF:** Wer einem Opfer einen eigenen, gültigen Bestätigungs- oder Reset-Link unterschiebt, kann es nach dem Knopfdruck im Angreifer-Konto anmelden (gilt in App und Web gleichermaßen, besteht schon heute im Web) | Opfer erfasst Daten im falschen Konto | **Bewusst akzeptiert** (F3): Der Knopfdruck auf einer Seite mit klarem Titel ist eine bewusste Handlung; der Schaden ist gering (Angreifer sieht, was das Opfer im fremden Konto anlegt). Kein zusätzlicher Dialog ohne zweiten Anwendungsfall |
|
||||||
|
|
||||||
|
### 9.2 Offene Fragen (Entscheidung Chris)
|
||||||
|
|
||||||
|
| # | Frage | Empfehlung | betrifft |
|
||||||
|
|---|---|---|---|
|
||||||
|
| F1 | Welche Pfade beansprucht die App? Nur `/auth/confirm` oder alle Web-Adressen? | **Nur `/auth/confirm`.** Erweiterung (z. B. Spieldetail-Links) erst, wenn es teilbare Inhalte gibt und die App die Route kennt. | E2, APPLINK-02, R6 |
|
||||||
|
| F2 | Passwortspeicher von App und Web verbinden (`webcredentials` iOS, `get_login_creds` Android)? | **Ja für iOS** (`webcredentials`, gleiche Datei, kein Mehraufwand außer Entitlement-Zeile; Passwort-Vorschläge der Web-Anmeldung erscheinen in der App). Android `get_login_creds` optional, nein in #49. | 4.3, 5.2 |
|
||||||
|
| F3 | Was passiert, wenn ein Link geöffnet wird, während in der App ein anderes Konto angemeldet ist? | **Wie im Web:** Nach Knopfdruck gilt die neue Sitzung, kein zusätzlicher Dialog. Der Knopfdruck ist bereits eine bewusste Handlung; ein Dialog wäre neue UX ohne zweiten Anwendungsfall. Das damit verbundene Login-CSRF-Restrisiko (R14) wird bewusst akzeptiert; es besteht schon heute im Web. | 5.2, R14 |
|
||||||
|
| F4 | Braucht die App vor KTO-11 ein vorläufiges „Abmelden“ (z. B. für Gerätetests)? | **Nein.** Abmelden ist KTO-11 Kriterium 3; Tester installieren neu oder nutzen den Reset-Abbruch. | 5.1 |
|
||||||
|
| F5 | Eigene Staging-App-Builds mit Staging-Host (und Debug-Fingerabdruck in der Staging-`assetlinks.json`)? | **In #49 nein.** Ein Host je Build über die zentrale Property (E8); Staging-Builds entscheidet devops mit dem Hosting-Setup. Bis dahin Android lokal per `adb` (6.2) und iOS erst gegen Prod/Staging-Domain mit Entwicklermodus. | E8, APPLINK-01 |
|
||||||
|
| F6 | Erscheint der Hinweis zur Anmeldung auch im Web? | **Ja.** QUE-09 Kriterium 13 verlangt ihn für App und Web-Adresse; „Web unverändert“ gilt nur für die Mail-Links. | E1 |
|
||||||
|
| F7 | Wann wird #49 geschlossen: mit dem Merge von PR B oder erst nach der Gerätebestätigung (C6)? | **Nach C6.** PR B referenziert `Refs #49`; Chris schließt #49 nach der Gerätematrix. Alternativ: PR B schließt #49, Gerätetest wird ein eigenes Issue. | 8.2 |
|
||||||
|
| F8 | Wo liegen die Zuordnungsdateien im Repo? | **`app/web/.well-known/`**, wenn S1 (e) zeigt, dass Build und Hosting sie korrekt ausliefern; sonst `deploy/well-known/` mit Gateway-Regel. | E7, D1a |
|
||||||
|
| F9 | Für das Produktions-Setup (D1 aus Spec #1, ADR-0002) gibt es kein Forgejo-Issue. Sollen C4/C5 und D1a dort hinein? | **Ja:** Issue „Produktions-Setup nach ADR-0002“ anlegen (requirements-analyst oder devops), Chris-Aufgaben C4/C5 und die Gateway-Vorgaben dort bündeln; #49 verweist darauf. | 8.3 |
|
||||||
|
| F10 | Übernimmst du die Präzisierungen der Akzeptanzkriterien (§ 2, Vorschläge zu 1–4 und Kriterium 5)? | **Ja**, sie machen Kriterien 2–4 testbar und schreiben das Web-Verhalten fest. Danach Issue-Text anpassen. | § 2 |
|
||||||
|
| F11 | `minSdk` auf 31 (Android 12) anheben, damit App Links nur mit verlässlichem Abfangschutz laufen? | **Nein in #49.** Android 7–11 schließt sonst Nutzer aus; E3 (Knopfdruck, einmalig, 1 h) begrenzt den Schaden (R13). Neu bewerten, wenn die Store-Statistik kaum Geräte unter Android 12 zeigt. | R13, M10 |
|
||||||
|
|
||||||
|
## 10. Änderungsprotokoll
|
||||||
|
|
||||||
|
| Datum | Änderung |
|
||||||
|
|---|---|
|
||||||
|
| 2026-11-03 | Entwurf (architect) |
|
||||||
|
| 2026-11-03 | Review code-reviewer eingearbeitet: vorhandenes Team `KSX6GJY9C5` (C1, APPLINK-03), `CODE_SIGN_ENTITLEMENTS` erst nach C1 bzw. über xcconfig-Weiche, E6 eingeschränkt, lokale Builds als Pflicht (A2, T2); Ablösung von Spec #1 E4/§ 6 in § 1 und als Aufgabe W2 dokumentiert (Verweise in Spec #1 selbst noch offen), Abweichung vom Issue-Vorschlag (`/reset-password`) festgehalten; `email_change` braucht Ergänzung in KTO-11 (E2); belegte Fakten aus dem Quelltext von Spike-Annahmen getrennt (E5, § 5.2); Outlook einheitlich „voraussichtlich“; QUE-06 Krit. 4 in der Matrix, T2 abhängig von A1; Abfangschutz eingeschränkt (R13, F11, M10), Login-CSRF akzeptiert (R14); `?mode=developer`-Mechanismus, HomeScreen liest nur den Provider, Import `home` → `auth` begründet, `key.properties` nur prüfen, Quelle Debug-Kaltstart |
|
||||||
Loading…
Reference in a new issue