## Server (melo_cloud.py v3.0.0) - Neue DB-Tabellen: user_playlists, user_playlist_songs, user_favorites, user_history, sync_meta - Playlist-Endpunkte: GET/POST /api/cloud/playlists, GET /api/cloud/playlists/<id>, POST songs, DELETE songs, PUT positions - Favoriten-Endpunkte: GET/POST /api/cloud/favorites, POST /api/cloud/favorites/toggle - History-Endpunkte: GET/POST /api/cloud/history (mit Dedup) - Rename-Endpunkt: POST /api/cloud/rename (custom_title/custom_artist) - Sync-Endpunkte: POST /api/cloud/sync-all, GET /api/cloud/sync-status - delete-all erweitert um Playlists + Favorites + History ## App - db_helper.dart: Migration v4→v5 (cloud_id, cloud_title, cloud_artist, server_favorites, sync_metadata) - auth_service.dart: Token-Validierung beim Start (online check + Offline-Fallback), Persistent Login - cloud_service.dart: Neue Methoden für Playlists (CRUD), Favorites (get/sync/toggle), Rename, History, syncAll/syncStatus - cloud_screen.dart: Komplett-Rewrite mit Sync-Modus (Manuell/Auto), Intervall-Auswahl, Sync-Fortschritt-Anzeige, Playlist-Verwaltung, Favoriten-Anzeige + Umbenennen, Sync-Info-Karte, nächster Sync - main.dart: Auto-Sync beim App-Start (nur wenn konfiguriert + fällig) ## Flutter Analyze - 0 neue Issues — nur 7 pre-existing
188 lines
5.8 KiB
Dart
188 lines
5.8 KiB
Dart
import 'dart:convert';
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import '../config/app_config.dart';
|
||
import '../services/melo_logger.dart';
|
||
|
||
/// 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._();
|
||
|
||
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 {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
_token = prefs.getString('baka_token');
|
||
_user = prefs.getString('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 prefs.remove('baka_token');
|
||
await prefs.remove('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 http
|
||
.get(
|
||
Uri.parse('${AppConfig.authUrl}/verify'),
|
||
headers: authHeader,
|
||
)
|
||
.timeout(const Duration(seconds: 5));
|
||
return response.statusCode == 200;
|
||
} 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 http
|
||
.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) {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString('baka_token', _token!);
|
||
await prefs.setString('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 http
|
||
.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) {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString('baka_token', _token!);
|
||
await prefs.setString('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 = '';
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.remove('baka_token');
|
||
await prefs.remove('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);
|
||
}
|