This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/services/auth_service.dart
T
Dustin 5b5f72b548 v2.50.2 — Cloud/Auth-Tests (http-Mocks): LWW-Sync + Token-Ablauf-Pfade
## Testbarkeit (kein neues Package)
- CloudService: http.Client injizierbar (Konstruktor-Parameter, Default unverändert)
- AuthService: http.Client + TokenSpeicher injizierbar; SecureTokenSpeicher kapselt FlutterSecureStorage, @visibleForTesting AuthService.fuerTests
- Alle HTTP-Calls über _client statt Top-Level http.get/post

## test/cloud_service_test.dart (22 Tests)
- login: 200/401/Netzwerkfehler
- verbinde() ohne Benutzer → Status fehler, kein Request
- statusDaten: 200/401(Auth-Fehler)/Netzwerkfehler
- syncChanges LWW-Konflikt-Pfade: updated_at bleibt für Client-Vergleich erhalten, leere/fehlende Changes, 401 → [], Netzwerkfehler → [], since-Body
- syncAll (LWW-Schreibpfad): 200/401
- Playlists, Favoriten, History, listSongs, delete, globalList inkl. Fehlerpfade

## test/auth_service_test.dart (19 Tests)
- initialisieren: 200 → eingeloggt, 401 → Logout + Speicher gelöscht, Netzwerkfehler → Offline-Trust, 5xx → Token bleibt, ohne Token → kein Request
- tokenPruefen: 200/401/Netzwerkfehler/503
- login: ok mit Persistenz, 401, 200-ohne-Token (Fall-Through-Regression), Netzwerkfehler
- logout löscht Speicher, authHeader mit/ohne Bearer
2026-08-04 20:46:51 +02:00

222 lines
7.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../config/app_config.dart';
import '../services/melo_logger.dart';
/// Minimal-Speicherabstraktion für die Token-Persistenz — macht den Service
/// in Tests mit einem In-Memory-Fake testbar (kein Platform-Channel nötig).
abstract class TokenSpeicher {
Future<String?> lesen(String key);
Future<void> schreiben(String key, String? wert);
Future<void> loeschen(String key);
}
/// Produktions-Implementierung auf FlutterSecureStorage.
class SecureTokenSpeicher implements TokenSpeicher {
final FlutterSecureStorage _secure = const FlutterSecureStorage();
@override
Future<String?> lesen(String key) => _secure.read(key: key);
@override
Future<void> schreiben(String key, String? wert) =>
_secure.write(key: key, value: wert);
@override
Future<void> loeschen(String key) => _secure.delete(key: key);
}
/// Baka-Auth Service JWT-basierte Authentifizierung (v3 — Persistent Login)
/// Integriert mit https://baka-net.de/auth
class AuthService {
static final AuthService _instance = AuthService._();
factory AuthService() => _instance;
AuthService._({http.Client? client, TokenSpeicher? speicher})
: _client = client ?? http.Client(),
_speicher = speicher ?? SecureTokenSpeicher();
/// Nur für Tests: frische Instanz mit injizierbaren Abhängigkeiten.
@visibleForTesting
AuthService.fuerTests({http.Client? client, TokenSpeicher? speicher})
: this._(client: client, speicher: speicher);
final http.Client _client;
final TokenSpeicher _speicher;
String? _token;
String _user = '';
bool _initialisiert = false;
bool get istEingeloggt => _token != null && _token!.isNotEmpty;
String get benutzer => _user;
String? get token => _token;
/// Auth-Header für API-Requests
Map<String, String> get authHeader {
final headers = <String, String>{
'Content-Type': 'application/json',
};
if (_token != null && _token!.isNotEmpty) {
headers['Authorization'] = 'Bearer $_token';
}
return headers;
}
/// Lädt gespeicherten Token beim App-Start und validiert ihn
Future<bool> initialisieren() async {
if (_initialisiert) return istEingeloggt;
try {
_token = await _speicher.lesen('baka_token');
_user = await _speicher.lesen('baka_user') ?? '';
if (_token != null && _token!.isNotEmpty) {
// Token beim Server validieren (nicht blind vertrauen)
final valid = await _tokenOnlinePruefen();
if (!valid) {
// Token ist abgelaufen — nicht als eingeloggt behandeln
MeloLogger().zustand('auth_token_expired', {'user': _user});
_token = null;
_user = '';
await _speicher.loeschen('baka_token');
await _speicher.loeschen('baka_user');
} else {
MeloLogger().zustand('auth_restored', {'user': _user});
}
}
} catch (e) {
MeloLogger().fehler('auth_init', e);
}
_initialisiert = true;
return istEingeloggt;
}
/// Prüft Token beim Baka-Auth-Server (online)
Future<bool> _tokenOnlinePruefen() async {
if (_token == null) return false;
try {
final response = await _client
.get(
Uri.parse('${AppConfig.authUrl}/verify'),
headers: authHeader,
)
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) return true;
// Nur bei explizitem 401 (abgelaufen/ungültig) den Token löschen.
// Bei 5xx, anderen Fehlern oder temporären Server-Problemen Token behalten.
if (response.statusCode == 401) return false;
return true;
} catch (_) {
// Bei Netzwerkfehler: Token lokal akzeptieren (Offline-Modus)
return true;
}
}
/// Login über Baka-Auth-Server
Future<AuthResult> login(String user, String password) async {
try {
final response = await _client
.post(
Uri.parse('${AppConfig.authUrl}/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'username': user,
'password': password,
}),
)
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
_token = data['token'] as String?;
_user = user;
if (_token != null && _token!.isNotEmpty) {
await _speicher.schreiben('baka_token', _token!);
await _speicher.schreiben('baka_user', _user);
MeloLogger().aktion('auth_login_ok', {'user': _user});
return AuthResult.ok;
}
final serverMsg = data['message'] as String? ??
data['error'] as String? ??
'Unbekannt';
return AuthResult.fehlgeschlagen(serverMsg);
}
String fehler = 'Login fehlgeschlagen (${response.statusCode})';
MeloLogger().fehler('auth_login_fail', fehler);
return AuthResult.fehlgeschlagen(fehler);
} catch (e) {
final msg = 'Keine Verbindung zum Auth-Server';
MeloLogger().fehler('auth_login_error', e);
return AuthResult.fehlgeschlagen(msg);
}
}
/// Registrierung über Baka-Auth-Server
Future<AuthResult> registrieren(
String user, String password, String email) async {
try {
final response = await _client
.post(
Uri.parse('${AppConfig.authUrl}/register'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'username': user,
'password': password,
'email': email,
}),
)
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200 || response.statusCode == 201) {
final data = jsonDecode(response.body);
_token = data['token'] as String?;
_user = user;
if (_token != null && _token!.isNotEmpty) {
await _speicher.schreiben('baka_token', _token!);
await _speicher.schreiben('baka_user', _user);
MeloLogger().aktion('auth_register_ok', {'user': _user});
return AuthResult.ok;
}
}
String fehler = 'Registrierung fehlgeschlagen';
try {
final data = jsonDecode(response.body);
fehler = data['error'] as String? ?? fehler;
} catch (_) {}
return AuthResult.fehlgeschlagen(fehler);
} catch (e) {
return AuthResult.fehlgeschlagen('Keine Verbindung zum Server');
}
}
/// Token beim Server validieren (public)
Future<bool> tokenPruefen() async {
return _tokenOnlinePruefen();
}
/// Ausloggen Token löschen
Future<void> logout() async {
_token = null;
_user = '';
await _speicher.loeschen('baka_token');
await _speicher.loeschen('baka_user');
MeloLogger().aktion('auth_logout', {});
}
}
/// Ergebnis eines Auth-Versuchs
class AuthResult {
final bool erfolg;
final String? fehler;
const AuthResult._(this.erfolg, this.fehler);
static const ok = AuthResult._(true, null);
static AuthResult fehlgeschlagen(String msg) => AuthResult._(false, msg);
}