import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; import 'navidrome_service.dart'; /// Ablage für den Anmelde-Token. Als Schnittstelle, damit Tests ohne die /// Plattform-Kanäle von FlutterSecureStorage auskommen. abstract class TokenSpeicher { Future lesen(String key); Future schreiben(String key, String wert); Future loeschen(String key); } /// Produktions-Ablage: verschlüsselter Gerätespeicher. class SicherenSpeicher implements TokenSpeicher { const SicherenSpeicher(); static const _secure = FlutterSecureStorage(); @override Future lesen(String key) => _secure.read(key: key); @override Future schreiben(String key, String wert) => _secure.write(key: key, value: wert); @override Future loeschen(String key) => _secure.delete(key: key); } /// Anmeldung am Baka-Konto. Der YouTube-Proxy (yt.baka-net.de) akzeptiert /// nur Anfragen mit dem hier erzeugten Bearer-Token. class BakaAuth extends ChangeNotifier { BakaAuth({http.Client? client, TokenSpeicher? speicher}) : _client = client ?? http.Client(), _speicher = speicher ?? const SicherenSpeicher(); static const basisUrl = 'https://baka-net.de/auth'; static const _tokenKey = 'baka_token'; static const _userKey = 'baka_user'; final http.Client _client; final TokenSpeicher _speicher; String? _token; String _benutzer = ''; int? _verbleibend; bool get istAngemeldet => _token != null && _token!.isNotEmpty; String get benutzer => _benutzer; /// Zuletzt vom Server gemeldeter Kontingent-Stand (100/Tag) — nur fürs /// Anzeigen, die Wahrheit bleibt beim Server. `null` vor dem ersten /// Download in dieser Session. int? get verbleibend => _verbleibend; /// Header für Anfragen an den Proxy. Ohne Anmeldung ohne `Authorization`. Map get authHeader => { if (istAngemeldet) 'Authorization': 'Bearer $_token', }; /// Stellt eine frühere Anmeldung wieder her (App-Start). Future laden() async { _token = await _speicher.lesen(_tokenKey); _benutzer = await _speicher.lesen(_userKey) ?? ''; notifyListeners(); } /// Meldet an. Gibt `null` bei Erfolg zurück, sonst den Fehlertext. Future anmelden(String benutzer, String passwort) async { final http.Response antwort; try { antwort = await _client .post( Uri.parse('$basisUrl/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'username': benutzer, 'password': passwort}), ) .timeout(const Duration(seconds: 15)); } catch (e) { debugPrint('Baka-Anmeldung fehlgeschlagen: $e'); return 'Keine Verbindung zum Anmelde-Server'; } Map daten; try { daten = jsonDecode(antwort.body) as Map; } catch (_) { return 'Anmelde-Server antwortet unverständlich (${antwort.statusCode})'; } final token = daten['token'] as String?; if (token != null && token.isNotEmpty) { _token = token; _benutzer = benutzer; await _speicher.schreiben(_tokenKey, token); await _speicher.schreiben(_userKey, benutzer); notifyListeners(); return null; } // Der Server meldet Fehler mit Status 200 und einem Text im Rumpf. return daten['message'] as String? ?? daten['error'] as String? ?? 'Anmeldung fehlgeschlagen (${antwort.statusCode})'; } /// Meldet automatisch an, falls noch nicht angemeldet — für Server-User /// (Dustin, Baka, Tinker), die keine eigene Baka-Anmeldung mehr sehen /// sollen: der Proxy nutzt seine YouTube-Cookies für jeden /// authentifizierten Aufruf ohnehin automatisch. Future autoAnmelden(String benutzer, String passwort) async { if (istAngemeldet || benutzer.isEmpty || passwort.isEmpty) return; await anmelden(benutzer, passwort); } /// Übernimmt den zuletzt vom Server gemeldeten Kontingent-Stand. void merkeVerbleibend(int n) { _verbleibend = n; notifyListeners(); } Future abmelden() async { _token = null; _benutzer = ''; _verbleibend = null; await _speicher.loeschen(_tokenKey); await _speicher.loeschen(_userKey); notifyListeners(); } } /// Prüft, ob ein Server-User verbunden ist, und meldet ihn dann automatisch /// beim Baka-Konto an — geteilte Logik für den YouTube-Bereich /// (downloads_screen.dart) und die YT-Suche (youtube_search_screen.dart). /// Gäste (kein Server-Account) bleiben unangetastet. Schlägt die /// Hintergrund-Anmeldung fehl (falsche/fehlende Zugangsdaten, Server nicht /// erreichbar), meldet [autoLoginFehlgeschlagen] das zurück — sonst bliebe /// so ein User ohne jeden Weg mehr zur manuellen Baka-Anmeldung. Future<({bool serverUser, bool autoLoginFehlgeschlagen})> pruefeServerUser( NavidromeService navidrome, BakaAuth auth) async { await navidrome.ladeGespeicherteZugangsdaten(); if (!navidrome.istVerbunden) { return (serverUser: false, autoLoginFehlgeschlagen: false); } await auth.autoAnmelden(navidrome.benutzer, navidrome.passwort); return (serverUser: true, autoLoginFehlgeschlagen: !auth.istAngemeldet); }