v2.31 — Login, Einstellungen & Cloud Sync Redesign

- Login-Screen im Melo-Design (Schwarz+Rot) mit Baka-Auth JWT
- AuthService fuer Token-Management ueber baka-net.de/auth
- Registrierungs-Modus + 'Ohne Login fortfahren'
- Navidrome-Credentials optional im Login aufklappbar

- SettingsScreen ersetzt AlertDialog
- BottomNav navigiert statt Dialog zu triggern
- Cloud Sync, Server verbinden, Diagnosedaten, App-Info, Abmelden

- CloudEinstellungen-Dialog entfernt, alles in CloudScreen konsolidiert
- Hardcode login('Baka','') entfernt → AuthService.benutzer
- Gradient-Statuskarte, animierte Segment-Intervall-Auswahl
- JWT Authorization statt Klartext-Passwort

- AppConfig: API-Key defaultValue entfernt (kein Fallback-Leak)
- Build crasht ohne --dart-define MELO_API_KEY

Build: split APK (arm64 19.6M, armeabi 17.1M, x86_64 21.1M)
This commit is contained in:
Dustin
2026-08-01 15:03:22 +02:00
parent 77a20422be
commit 36c744d6e3
77 changed files with 5211 additions and 1067 deletions
+26 -109
View File
@@ -1,102 +1,45 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.dart';
import '../services/auth_service.dart';
import '../services/melo_logger.dart';
/// Cloud-Sync Service für Melo Registry.
/// Auth: Bearer-JWT vom Baka-Auth-Server (Login mit Nutzername + Passwort).
/// Der alte X-API-Key/X-User-Mechanismus wurde entfernt (IDOR-Lücke).
/// Cloud-Sync Service für Melo Registry
/// Authentifizierung via Baka-Auth JWT-Token
class CloudService {
static final CloudService _instanz = CloudService._();
factory CloudService() => _instanz;
CloudService._();
static String get _base => AppConfig.cloudUrl;
static String get _authBase => AppConfig.authUrl;
String _user = '';
String _token = '';
/// Token liegt verschlüsselt im Keychain/Keystore (flutter_secure_storage) —
/// nicht mehr im Klartext in SharedPreferences (Security-Audit CRIT-1).
static const _secure = FlutterSecureStorage();
String get user => _user;
String get token => _token;
bool get istAngemeldet => _token.isNotEmpty;
/// Echter Login gegen den Baka-Auth-Server.
/// Der Token wird gespeichert und bei allen Cloud-Calls als
/// Authorization: Bearer `token` mitgeschickt.
Future<bool> login(String user, String pass) async {
/// Login mit Baka-Auth Token wird aus AuthService bezogen
Future<bool> login(String user) async {
_user = user;
try {
final r = await http
.post(Uri.parse('$_authBase/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'username': user, 'password': pass}))
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) {
final d = jsonDecode(r.body);
if (d['status'] == 'ok' && d['token'] != null) {
_user = d['username'] as String? ?? user;
_token = d['token'] as String;
await _speichereToken();
MeloLogger.cloudToken = _token;
return true;
}
}
} catch (_) {}
return false;
}
/// Stellt gespeicherten Token wieder her (Auto-Login nach App-Start).
/// Migriert einmalig alte SharedPreferences-Einträge in SecureStorage.
Future<bool> restoreLogin() async {
final prefs = await SharedPreferences.getInstance();
var t = await _secure.read(key: 'melo_cloud_token') ?? '';
var u = await _secure.read(key: 'melo_cloud_user') ?? '';
// Migration alter Versionen (Token lag früher in SharedPreferences)
if (t.isEmpty) {
final altT = prefs.getString('melo_cloud_token') ?? '';
final altU = prefs.getString('melo_cloud_user') ?? '';
if (altT.isNotEmpty) {
t = altT;
u = altU;
await _secure.write(key: 'melo_cloud_token', value: t);
if (u.isNotEmpty) {
await _secure.write(key: 'melo_cloud_user', value: u);
}
await prefs.remove('melo_cloud_token');
await prefs.remove('melo_cloud_user');
}
.get(Uri.parse('$_base/api/cloud/status'),
headers: _authHeader)
.timeout(const Duration(seconds: 5));
return r.statusCode == 200;
} catch (_) {
return false;
}
if (t.isEmpty) return false;
_token = t;
_user = u;
MeloLogger.cloudToken = t;
return true;
}
Future<void> logout() async {
_token = '';
_user = '';
MeloLogger.cloudToken = null;
await _secure.delete(key: 'melo_cloud_token');
await _secure.delete(key: 'melo_cloud_user');
Map<String, String> get _authHeader {
final token = AuthService().token;
final headers = <String, String>{
'X-API-Key': AppConfig.ytProxyApiKey,
};
if (_user.isNotEmpty) {
headers['X-User'] = _user;
}
// Baka-Auth JWT Token mitsenden falls vorhanden
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
return headers;
}
Future<void> _speichereToken() async {
await _secure.write(key: 'melo_cloud_token', value: _token);
await _secure.write(key: 'melo_cloud_user', value: _user);
}
Map<String, String> get _authHeader => {
if (_token.isNotEmpty) 'Authorization': 'Bearer $_token',
};
Future<Map?> status() => _get('/api/cloud/status');
Future<List<Map>> listSongs() async {
@@ -126,11 +69,7 @@ class CloudService {
headers: _authHeader)
.timeout(const Duration(seconds: 120));
if (r.statusCode == 200) {
final file = File(destPath);
if (!await file.parent.exists()) {
await file.parent.create(recursive: true);
}
await file.writeAsBytes(r.bodyBytes);
await File(destPath).writeAsBytes(r.bodyBytes);
return true;
}
return false;
@@ -229,26 +168,4 @@ class CloudService {
return false;
}
}
/// Löscht alle Cloud-Songs des angemeldeten Users (inkl. Server-Dateien).
Future<bool> loescheMusik() async {
return _postOhneBody('/api/cloud/delete-music');
}
/// Löscht ALLE Cloud-Daten des Users (Musik + Shares + Ordner).
Future<bool> loescheAlles() async {
return _postOhneBody('/api/cloud/delete-all');
}
Future<bool> _postOhneBody(String path) async {
try {
final r = await http
.post(Uri.parse('$_base$path'),
headers: {..._authHeader, 'Content-Type': 'application/json'})
.timeout(const Duration(seconds: 30));
return r.statusCode == 200;
} catch (_) {
return false;
}
}
}