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 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 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 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 _speichereToken() async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('melo_cloud_token', _token); await prefs.setString('melo_cloud_user', _user); } Map get _authHeader => { if (_token.isNotEmpty) 'Authorization': 'Bearer $_token', }; Future status() => _get('/api/cloud/status'); Future> listSongs() async { final r = await _get('/api/cloud/list'); return List.from(r?['songs'] ?? []); } Future 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 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 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 _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 share(List 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 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> 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.from(d['changes'] ?? []); } } catch (_) {} return []; } Future> 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.from(jsonDecode(r.body)['songs'] ?? []); } } catch (_) {} return []; } Future 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; } } }