fix: Gast-Zugang-Sperre, veralteter Token, Doppel-Tap, Hinweistext (Branch-Review)
Behebt die Findings aus dem abschließenden Gesamt-Branch-Review für feature/youtube-gast-zugang, alle in einem Rutsch: - KRITISCH: Gast-Modus war eine Einbahnstraße — einmal "Als Gast fortfahren" getippt, war _AnmeldeDialog (einziger Baka-Login-Einstieg, auch für Melo-Cloud-Sync) dauerhaft unerreichbar. Neu: GastZugang.verwerfen() plus "Anmelden"-Knopf neben der Gast-Statuszeile in beiden Screens. - WICHTIG: zugriffOk prüfte den rohen gast.hatToken statt istGastModus — ein alter Gast-Token konnte den Auto-Login-Rückfallweg für Server-User verdecken. istGastModus wird jetzt vor zugriffOk berechnet und dort verwendet. - WICHTIG: CHANGELOG ergänzt — der Server-Endpunkt POST /api/guest-token fehlt noch (separates Vorhaben). - Doppel-Tap-Schutz für den Gast-Button in beiden Screens. - Widersprüchlicher Hinweistext im YT-Suche-Tab (verlangte Baka-Login direkt über dem Gast-Button) korrigiert. - Zwei Gast-Tests in youtube_search_screen_test.dart bekommen jetzt echtes gast:-Wiring in YtSearchService/YtDownloadService. - 2 neue Regressionstests fürs Anmelden-Escape-Hatch. Getestet: volle Suite grün (602 Tests, 1 skipped, 0 failed), flutter analyze ohne Befund. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcDiyJdVRqh1TtJk5JiabX
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b32f3198f8
commit
d20f01adee
@@ -5,8 +5,47 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 🐛 Gast-Zugang: Fixe aus dem Branch-Abschluss-Review (2026-08-26)
|
||||
|
||||
- 🔓 **Gast-Modus war eine Einbahnstraße.** Einmal "Als Gast fortfahren"
|
||||
angetippt, blieb `gast.hatToken` dauerhaft `true` (Token steckt im
|
||||
Secure Storage, wird bei jedem App-Start wiederhergestellt) — und damit
|
||||
war `_AnmeldeDialog`, der EINZIGE Einstieg in der App für die
|
||||
Baka-Anmeldung (auch für den Melo-Cloud-Sync-Login!), unerreichbar.
|
||||
Neu: `GastZugang.verwerfen()` verwirft den Token wieder, und ein
|
||||
"Anmelden"-Knopf neben der Gast-Statuszeile (Download-Tab und
|
||||
YT-Suche-Tab) ruft ihn auf.
|
||||
- 🔀 **Ein alter Gast-Token hat den Auto-Login-Rückfallweg für
|
||||
Server-User verdeckt.** `zugriffOk` prüfte bisher den rohen
|
||||
`gast.hatToken` statt `istGastModus` — ein Gerät, das früher mal als
|
||||
Gast lief und jetzt als Server-User mit fehlgeschlagenem Auto-Login
|
||||
dasteht, bekam die Fehlermeldung dazu nie zu sehen und lud still über
|
||||
den (limitierten) Gast-Pfad. `istGastModus` wird jetzt vor `zugriffOk`
|
||||
berechnet und dort statt des rohen Tokens verwendet.
|
||||
(`lib/downloads/downloads_screen.dart`,
|
||||
`lib/downloads/youtube_search_screen.dart`)
|
||||
- ⏱️ **Doppel-Tap auf "Als Gast fortfahren" konnte zwei Anfragen
|
||||
gleichzeitig auslösen.** Beide Screens sperren den Button jetzt während
|
||||
`holeToken()` läuft (`_gastLaeuft`).
|
||||
- 📝 **Widersprüchlicher Hinweistext im YT-Suche-Tab behoben** — der Text
|
||||
über dem Gast-Button verlangte fälschlich eine Baka-Anmeldung.
|
||||
- ⚠️ **CHANGELOG ergänzt:** der Abschnitt zum Gast-Zugang unten macht jetzt
|
||||
klar, dass die Server-Seite (`POST /api/guest-token`) noch fehlt.
|
||||
- ✅ **Getestet:** 2 neue Regressionstests fürs "Anmelden"-Escape-Hatch
|
||||
(`test/downloads/online_screen_test.dart`,
|
||||
`test/downloads/youtube_search_screen_test.dart`), 2 bestehende Gast-Tests
|
||||
im YT-Suche-Tab jetzt mit echtem `gast:`-Wiring in
|
||||
`YtSearchService`/`YtDownloadService`. Volle Suite grün, `flutter
|
||||
analyze` ohne Befund.
|
||||
|
||||
### 📺👤 YouTube-Gast-Zugang: bewusste Wahl + Tages-Zähler (Download-Tab + YT-Suche-Tab) (2026-08-26)
|
||||
|
||||
- ⚠️ **Braucht noch die Server-Seite.** Der neue Endpunkt `POST
|
||||
/api/guest-token` und die Quota-Logik in `yt_proxy.py` existieren noch
|
||||
nicht (separates Vorhaben, siehe
|
||||
`docs/superpowers/specs/2026-08-26-youtube-guest-quota-design.md`). Bis
|
||||
die Server-Seite steht, liefert der "Als Gast fortfahren"-Button
|
||||
zuverlässig einen 404-Fehler.
|
||||
- 👤 **Echte Gäste (kein Baka-Konto, kein Melo-Server) sehen jetzt zwei
|
||||
Buttons** statt nur der Anmeldung: "Beim Baka-Konto anmelden" und neu
|
||||
"Als Gast fortfahren (5 Downloads/Tag)". Der Gast-Button holt sich per
|
||||
|
||||
@@ -619,6 +619,9 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
||||
/// Fehlertext, falls das Holen des Gast-Tokens scheitert.
|
||||
String? _gastFehler;
|
||||
|
||||
/// Verhindert Mehrfach-Anfragen bei schnellem Doppel-Antippen.
|
||||
bool _gastLaeuft = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -690,11 +693,18 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
||||
}
|
||||
|
||||
Future<void> _alsGastFortfahren() async {
|
||||
if (_gastLaeuft) return;
|
||||
final gast = context.read<GastZugang>();
|
||||
setState(() => _gastFehler = null);
|
||||
setState(() {
|
||||
_gastFehler = null;
|
||||
_gastLaeuft = true;
|
||||
});
|
||||
final fehler = await gast.holeToken();
|
||||
if (!mounted) return;
|
||||
if (fehler != null) setState(() => _gastFehler = fehler);
|
||||
setState(() {
|
||||
_gastLaeuft = false;
|
||||
if (fehler != null) _gastFehler = fehler;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _herunterladen() async {
|
||||
@@ -755,10 +765,10 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
||||
// Anmeldung nachweislich fehlgeschlagen ist, bleibt der manuelle Weg
|
||||
// als Rückfalloption sichtbar — sonst gäbe es keinen Weg mehr zurück.
|
||||
// Ein Gast-Token zählt ebenfalls als Zugang, nur mit Tages-Limit.
|
||||
final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken;
|
||||
final zugriffOk = auth.istAngemeldet ||
|
||||
(_serverUser && !_autoLoginFehlgeschlagen) ||
|
||||
gast.hatToken;
|
||||
final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken;
|
||||
istGastModus;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||
@@ -794,7 +804,7 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
||||
icon: const Icon(Icons.person_outline),
|
||||
label: const Text('Als Gast fortfahren (5 Downloads/Tag)',
|
||||
style: TextStyle(fontSize: 16)),
|
||||
onPressed: _alsGastFortfahren,
|
||||
onPressed: _gastLaeuft ? null : _alsGastFortfahren,
|
||||
),
|
||||
),
|
||||
if (_gastFehler != null) ...[
|
||||
@@ -834,6 +844,10 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
||||
style: const TextStyle(color: MeloTheme.text2, fontSize: 13),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: gast.verwerfen,
|
||||
child: const Text('Anmelden'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -38,6 +38,9 @@ class _YoutubeSearchScreenState extends State<YoutubeSearchScreen> {
|
||||
/// Fehlertext, falls das Holen des Gast-Tokens scheitert.
|
||||
String? _gastFehler;
|
||||
|
||||
/// Verhindert Mehrfach-Anfragen bei schnellem Doppel-Antippen.
|
||||
bool _gastLaeuft = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -56,11 +59,18 @@ class _YoutubeSearchScreenState extends State<YoutubeSearchScreen> {
|
||||
}
|
||||
|
||||
Future<void> _alsGastFortfahren() async {
|
||||
if (_gastLaeuft) return;
|
||||
final gast = context.read<GastZugang>();
|
||||
setState(() => _gastFehler = null);
|
||||
setState(() {
|
||||
_gastFehler = null;
|
||||
_gastLaeuft = true;
|
||||
});
|
||||
final fehler = await gast.holeToken();
|
||||
if (!mounted) return;
|
||||
if (fehler != null) setState(() => _gastFehler = fehler);
|
||||
setState(() {
|
||||
_gastLaeuft = false;
|
||||
if (fehler != null) _gastFehler = fehler;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -81,10 +91,10 @@ class _YoutubeSearchScreenState extends State<YoutubeSearchScreen> {
|
||||
final auth = context.watch<BakaAuth>();
|
||||
final gast = context.watch<GastZugang>();
|
||||
final suche = context.watch<YtSearchService>();
|
||||
final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken;
|
||||
final zugriffOk = auth.istAngemeldet ||
|
||||
(_serverUser && !_autoLoginFehlgeschlagen) ||
|
||||
gast.hatToken;
|
||||
final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken;
|
||||
istGastModus;
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
@@ -116,8 +126,12 @@ class _YoutubeSearchScreenState extends State<YoutubeSearchScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'Die Suche läuft über den Baka-Server. Dafür brauchst du '
|
||||
'deine Baka-Anmeldung (siehe Download-Tab).',
|
||||
_serverUser
|
||||
? 'Die Suche läuft über den Baka-Server. Dafür brauchst '
|
||||
'du deine Baka-Anmeldung (siehe Download-Tab).'
|
||||
: 'Die Suche läuft über den Baka-Server. Melde dich mit '
|
||||
'deinem Baka-Konto an (siehe Download-Tab) oder '
|
||||
'nutze sie eingeschränkt als Gast.',
|
||||
style: const TextStyle(color: MeloTheme.text2, fontSize: 13),
|
||||
),
|
||||
),
|
||||
@@ -131,7 +145,7 @@ class _YoutubeSearchScreenState extends State<YoutubeSearchScreen> {
|
||||
icon: const Icon(Icons.person_outline),
|
||||
label: const Text('Als Gast fortfahren (5 Downloads/Tag)',
|
||||
style: TextStyle(fontSize: 16)),
|
||||
onPressed: _alsGastFortfahren,
|
||||
onPressed: _gastLaeuft ? null : _alsGastFortfahren,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -148,11 +162,21 @@ class _YoutubeSearchScreenState extends State<YoutubeSearchScreen> {
|
||||
if (istGastModus)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
gast.verbleibend == null
|
||||
? 'Als Gast unterwegs (5 Downloads/Tag)'
|
||||
: 'Als Gast unterwegs — noch ${gast.verbleibend} von 5 heute',
|
||||
style: const TextStyle(color: MeloTheme.text2, fontSize: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
gast.verbleibend == null
|
||||
? 'Als Gast unterwegs (5 Downloads/Tag)'
|
||||
: 'Als Gast unterwegs — noch ${gast.verbleibend} von 5 heute',
|
||||
style: const TextStyle(color: MeloTheme.text2, fontSize: 13),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: gast.verwerfen,
|
||||
child: const Text('Anmelden'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (suche.laeuft) const LinearProgressIndicator(),
|
||||
|
||||
@@ -79,4 +79,15 @@ class GastZugang extends ChangeNotifier {
|
||||
_verbleibend = n;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Verwirft den Gast-Zugang wieder — nötig, damit ein Gast doch noch zum
|
||||
/// Baka-Konto wechseln kann. Ohne das wäre `_AnmeldeDialog` (der einzige
|
||||
/// Einstieg in der App für Baka-Login, auch für den Melo-Cloud-Sync)
|
||||
/// dauerhaft unerreichbar, sobald einmal ein Gast-Token besteht.
|
||||
Future<void> verwerfen() async {
|
||||
_token = null;
|
||||
_verbleibend = null;
|
||||
await _speicher.loeschen(_tokenKey);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,4 +334,32 @@ void main() {
|
||||
expect(find.text('Gast-Zugang fehlgeschlagen (500)'), findsOneWidget);
|
||||
expect(find.text('YouTube-Adresse'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Anmelden im Gast-Modus verwirft den Gast-Token und zeigt wieder die '
|
||||
'Wahl', (tester) async {
|
||||
final auth = BakaAuth(
|
||||
client: MockClient((_) async => http.Response('', 500)),
|
||||
speicher: _MemorySpeicher());
|
||||
final gast = GastZugang(
|
||||
client: MockClient((_) async =>
|
||||
http.Response(jsonEncode({'guest_token': 'g-1'}), 200)),
|
||||
speicher: _MemorySpeicher(),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(auth, db: db, categories: categories, gast: gast));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('YouTube-Adresse'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Anmelden'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget);
|
||||
expect(find.text('YouTube-Adresse'), findsNothing);
|
||||
expect(gast.hatToken, isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ void main() {
|
||||
db: db, auth: auth, search: search, download: download));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Baka-Anmeldung'), findsOneWidget);
|
||||
expect(find.textContaining('Baka-Server'), findsOneWidget);
|
||||
expect(find.text('Nach Musikvideos suchen'), findsOneWidget);
|
||||
|
||||
await db.close();
|
||||
@@ -191,11 +191,15 @@ void main() {
|
||||
final auth = BakaAuth(
|
||||
client: MockClient((_) async => http.Response('', 500)),
|
||||
speicher: _MemorySpeicher());
|
||||
final search = YtSearchService(auth: auth);
|
||||
final download = YtDownloadService(auth: auth);
|
||||
final gast = GastZugang(
|
||||
client: MockClient((_) async => http.Response('', 500)),
|
||||
speicher: _MemorySpeicher(),
|
||||
);
|
||||
final search = YtSearchService(auth: auth, gast: gast);
|
||||
final download = YtDownloadService(auth: auth, gast: gast);
|
||||
|
||||
await tester.pumpWidget(_wrap(
|
||||
db: db, auth: auth, search: search, download: download));
|
||||
db: db, auth: auth, search: search, download: download, gast: gast));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -208,6 +212,34 @@ void main() {
|
||||
'Als Gast fortfahren blendet den Baka-Hinweis aus und zeigt den Zähler',
|
||||
(tester) async {
|
||||
final db = MeloDb(NativeDatabase.memory());
|
||||
final auth = BakaAuth(
|
||||
client: MockClient((_) async => http.Response('', 500)),
|
||||
speicher: _MemorySpeicher());
|
||||
final gast = GastZugang(
|
||||
client: MockClient((_) async =>
|
||||
http.Response(jsonEncode({'guest_token': 'g-1'}), 200)),
|
||||
speicher: _MemorySpeicher(),
|
||||
);
|
||||
final search = YtSearchService(auth: auth, gast: gast);
|
||||
final download = YtDownloadService(auth: auth, gast: gast);
|
||||
|
||||
await tester.pumpWidget(_wrap(
|
||||
db: db, auth: auth, search: search, download: download, gast: gast));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Baka-Anmeldung'), findsNothing);
|
||||
expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget);
|
||||
|
||||
await db.close();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Anmelden im Gast-Modus verwirft den Gast-Token und zeigt wieder den '
|
||||
'Hinweis', (tester) async {
|
||||
final db = MeloDb(NativeDatabase.memory());
|
||||
final auth = BakaAuth(
|
||||
client: MockClient((_) async => http.Response('', 500)),
|
||||
speicher: _MemorySpeicher());
|
||||
@@ -225,10 +257,14 @@ void main() {
|
||||
|
||||
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Baka-Anmeldung'), findsNothing);
|
||||
expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Anmelden'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Baka-Server'), findsOneWidget);
|
||||
expect(gast.hatToken, isFalse);
|
||||
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user