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/cloud_service.dart
T
Hermes (Server) b3aedc53f9 Feature: Cloud-Daten loeschen in Einstellungen (3s-Halten)
- Einstellungen > Cloud-Daten loeschen: 'Nur Musik' oder 'Komplett vom Server'
- 3-Sekunden-Gedrueckthalten-Button zur Bestaetigung (Fortschrittsbalken)
- Warnhinweis: Wird SOFORT geloescht, nicht rueckgaengig machbar
- Server: delete-music/delete-all Endpunkte mit Dedup-sicherem Registry-Cleanup
- Loeschen nur des eigenen Kontos (User aus Token, kein IDOR)
2026-07-31 15:39:19 +02:00

231 lines
7.2 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.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).
class CloudService {
static String get _base => AppConfig.cloudUrl;
static String get _authBase => AppConfig.authUrl;
String _user = '';
String _token = '';
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 {
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).
Future<bool> restoreLogin() async {
final prefs = await SharedPreferences.getInstance();
final t = prefs.getString('melo_cloud_token') ?? '';
if (t.isEmpty) return false;
_token = t;
_user = prefs.getString('melo_cloud_user') ?? '';
MeloLogger.cloudToken = t;
return true;
}
Future<void> logout() async {
_token = '';
_user = '';
MeloLogger.cloudToken = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('melo_cloud_token');
await prefs.remove('melo_cloud_user');
}
Future<void> _speichereToken() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('melo_cloud_token', _token);
await prefs.setString('melo_cloud_user', _user);
}
Map<String, String> get _authHeader => {
if (_token.isNotEmpty) 'Authorization': 'Bearer $_token',
};
Future<Map?> status() => _get('/api/cloud/status');
Future<List<Map>> listSongs() async {
final r = await _get('/api/cloud/list');
return List<Map>.from(r?['songs'] ?? []);
}
Future<String?> upload(String filepath, String filename) async {
try {
final req = http.MultipartRequest('POST', Uri.parse('$_base/api/cloud/upload'));
req.headers.addAll(_authHeader);
req.files.add(await http.MultipartFile.fromPath('file', filepath,
filename: filename));
final resp = await req.send().timeout(const Duration(seconds: 120));
final body = jsonDecode(await resp.stream.bytesToString());
return body['song_id'] as String?;
} catch (e) {
MeloLogger().fehler('cloud_upload', e);
return null;
}
}
Future<bool> download(String songId, String destPath) async {
try {
final r = await http
.get(Uri.parse('$_base/api/cloud/download/$songId'),
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);
return true;
}
return false;
} catch (e) {
MeloLogger().fehler('cloud_download', e);
return false;
}
}
Future<bool> delete(String songId) async {
try {
final r = await http
.post(Uri.parse('$_base/api/cloud/delete'),
headers: {..._authHeader, 'Content-Type': 'application/json'},
body: jsonEncode({'song_id': songId}))
.timeout(const Duration(seconds: 10));
return r.statusCode == 200;
} catch (_) {
return false;
}
}
Future<Map?> _get(String path) async {
try {
final r = await http
.get(Uri.parse('$_base$path'), headers: _authHeader)
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) return jsonDecode(r.body);
} catch (_) {}
return null;
}
Future<String?> share(List<String> songIds) async {
try {
final r = await http
.post(Uri.parse('$_base/api/cloud/share'),
headers: {..._authHeader, 'Content-Type': 'application/json'},
body: jsonEncode({'song_ids': songIds}))
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) {
final d = jsonDecode(r.body);
return d['code'] as String?;
}
} catch (_) {}
return null;
}
Future<Map?> importCode(String code) async {
try {
final r = await http
.post(Uri.parse('$_base/api/cloud/import'),
headers: {..._authHeader, 'Content-Type': 'application/json'},
body: jsonEncode({'code': code}))
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) return jsonDecode(r.body);
} catch (_) {}
return null;
}
Future<List<Map>> syncChanges(String since) async {
try {
final r = await http
.post(Uri.parse('$_base/api/cloud/sync'),
headers: {..._authHeader, 'Content-Type': 'application/json'},
body: jsonEncode({'since': since}))
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) {
final d = jsonDecode(r.body);
return List<Map>.from(d['changes'] ?? []);
}
} catch (_) {}
return [];
}
Future<List<Map>> globalList() async {
try {
final r = await http
.get(Uri.parse('$_base/api/cloud/global'), headers: _authHeader)
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) {
return List<Map>.from(jsonDecode(r.body)['songs'] ?? []);
}
} catch (_) {}
return [];
}
Future<bool> toggleGlobal(String songId) async {
try {
final r = await http
.post(Uri.parse('$_base/api/cloud/toggle-global'),
headers: {..._authHeader, 'Content-Type': 'application/json'},
body: jsonEncode({'song_id': songId}))
.timeout(const Duration(seconds: 10));
return r.statusCode == 200;
} catch (_) {
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;
}
}
}