Existing team KSX6GJY9C5 (C1), defer CODE_SIGN_ENTITLEMENTS until the team is confirmed, local mobile builds mandatory, email_change needs KTO-11, verified facts separated from spike assumptions, QUE-06 keyboard criterion, T2 depends on PR A, limited interception protection on Android < 12 (R13, F11), login CSRF accepted (R14), supersession of spec 1 E4 recorded as task W2. Refs #49 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
48 KiB
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):
- Die öffentliche Startseite (
HomeScreen) hat keinen Weg zu/loginoder/register.privateRoutePathsist leer, deshalb leitet auch kein Guard dorthin. Auf Mobilgeräten sind die Auth-Screens dadurch nicht erreichbar. - 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 zeigtSITE_URLaufhttp://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“.
- Auf iOS und Android ist die Anmeldung aus der App heraus erreichbar.
- Der Bestätigungslink aus der Mail öffnet die App und schließt die Registrierung ab.
- Der Passwort-Reset-Link öffnet die App und führt in den Reset-Ablauf.
- 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
/loginbzw./registerfü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):
{
"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:
[
{
"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). Keinfrom-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 alsSemantics(header: true). - Dumm: bekommt keine Logik, nur Navigation per Callback oder
context.go.
HomeScreen wird ConsumerWidget:
- Liest nur
showSignInPromptProvider(siehe unten), nichtauthStatusProviderdirekt. Der Provider bildet die Zustände so ab:signedOut→ Platzhaltertext und darunterSignInPrompt.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
signedOutbehandeln (Prompt anzeigen), Fehler nur ins Log.
- Die Entscheidung „Prompt ja/nein“ trifft ein kleiner Provider
showSignInPromptProvider(bool) infeatures/auth/presentation/providers/, derauthStatusProviderliest; 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 zuauth(Routen, Status);homeist nur Gastgeber. Die Abhängigkeit geht in eine Richtung (authkennthomenicht), und QUE-09 ersetzthomespäter durch die Startseite, die denselben Import nutzt. Ein Verschieben nachcore/widgetslohnt 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: inMainActivityzusätzlicher Intent-Filter mitandroid:autoVerify="true",action VIEW,category DEFAULTundBROWSABLE,data android:scheme="https" android:host="${appLinkHost}" android:path="/auth/confirm". Dazumeta-data flutter_deeplinking_enabled = true.launchMode="singleTop"bleibt (warme Links kommen überonNewIntent).android/app/build.gradle.kts:manifestPlaceholders["appLinkHost"]aus der Gradle-PropertyshuffleAppLinkHost(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_ENTITLEMENTSwird erst gesetzt, wenn C1 bestätigt, dassKSX6GJY9C5ein bezahltes Team ist. Bis dahin bleibt die Einstellung leer, oder sie kommt über eine xcconfig-Variable (z. B.SHUFFLE_ENTITLEMENTSinios/Flutter/AppLinks.xcconfig, Standard leer), die nach C1 aufRunner/Runner.entitlementsgesetzt wird. Für Tests auf Geräten im Entwicklermodus optionalapplinks:$(APP_LINK_HOST)?mode=developer(umgeht den Apple-CDN-Cache), nur in Debug. Mechanismus: der Suffix kommt aus einer VariablenAPP_LINK_MODE_SUFFIX, die nurDebug.xcconfigauf?mode=developersetzt (Release.xcconfiglässt sie leer); der Eintrag lautet dannapplinks:$(APP_LINK_HOST)$(APP_LINK_MODE_SUFFIX). Alternative: eigeneRunner-Debug.entitlementsnur für Debug.ios/Runner/Info.plist:FlutterDeepLinkingEnabled = true.ios/Runner/SceneDelegate.swift: unverändert, sofern S1 zeigt, dassFlutterSceneDelegateUniversal Links (scene(_:willConnectTo:options:)undscene(_: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 ausauthStatusProvider, 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 auflocalhost:3000, dort funktioniert der Web-Ablauf. - Android lokal prüfbar: Den
token_hashaus 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 (Envdev.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-associationund/.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 |