Security: Cloud-Auth auf Bearer-JWT umgestellt (IDOR-Luecke geschlossen)

- cloud_service: echter Login gegen baka-auth, Token statt X-API-Key/X-User
- app_config: hartcodierten API-Key-Default entfernt
- download_service + melo_logger: Bearer-Token statt X-API-Key
- navidrome: Passwort in flutter_secure_storage (Keychain/Keystore)
- song: token-haltige stream_url wird nicht mehr in SQLite persistiert
- cloud_screen: Pfad-Traversal beim Download-Dateinamen gefixt (p.basename)
- home_screen: Login-Dialog mit Passwort-Feld, Auto-Sync nutzt restoreLogin
This commit is contained in:
Hermes (Server)
2026-07-31 14:31:12 +02:00
parent 3e9389ab59
commit 9a880b0f21
17 changed files with 1552 additions and 181 deletions
+128 -33
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../database/db_helper.dart';
import '../services/player_service.dart';
@@ -6,7 +7,13 @@ import '../services/favoriten_service.dart';
import '../services/download_service.dart';
import '../services/navidrome_service.dart';
import '../services/playlist_service.dart';
import '../services/cloud_service.dart';
import '../services/melo_logger.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import '../models/song.dart';
import '../models/tag.dart';
class MeloHomeViewModel extends ChangeNotifier {
final PlayerService player = PlayerService();
@@ -16,6 +23,8 @@ class MeloHomeViewModel extends ChangeNotifier {
final DownloadService downloader = DownloadService();
final NavidromeService navidrome = NavidromeService();
final PlaylistService playlists = PlaylistService();
final CloudService cloud = CloudService();
Timer? _autoSyncTimer;
List<Song> songs = [];
Set<String> aktiveTags = {};
@@ -27,9 +36,39 @@ class MeloHomeViewModel extends ChangeNotifier {
bool zeigeBotschaft = false;
bool serverLadt = false;
List<SubsonicAlbum> navidromeAlben = [];
Map<String, Tag> _tagsMap = {}; // Name → Tag für Filter-Lookup
Map<String, Tag> get tagsMap => _tagsMap;
int _playCount = 0;
static const int _botschaftSchwellwert = 10;
String? _nutzerBotschaft;
/// Per-User Easteregg-Botschaften
static const _botschaften = {
'Baka': '💌 Seit 2008 Danke, dass du immer da bist ♥',
'Tinker': '🎀 Für meine beste Freundin Melo & Melo 💕',
};
String? get nutzerBotschaft => _nutzerBotschaft;
void setzeNutzer(String name) {
_nutzerBotschaft = _botschaften[name];
}
StreamSubscription<Duration>? _positionsSub;
int _letzteGespeicherteSekunde = -1;
void _startePositionTracking() {
_positionsSub?.cancel();
_letzteGespeicherteSekunde = -1;
_positionsSub = player.positionStream.listen((pos) {
final s = player.aktuellerSong;
final sek = pos.inSeconds;
if (s?.id != null && sek > 0 && sek % 10 == 0 && sek != _letzteGespeicherteSekunde) {
_letzteGespeicherteSekunde = sek;
db.positionAktualisieren(s!.id!, sek);
}
});
}
static const _defaultTags = [
{'name': 'Alle', 'icon': ''},
@@ -43,10 +82,18 @@ class MeloHomeViewModel extends ChangeNotifier {
List<Song> get gefilterteSongs {
if (aktiveTags.isEmpty) return songs;
return songs.where((s) =>
aktiveTags.any((tag) =>
s.titel.contains(tag) || s.kuenstler.contains(tag))
).toList();
// Favoriten-Filter
if (aktiveTags.contains('★ Favoriten')) {
return songs.where((s) => s.id != null && favoritenIds.contains(s.id)).toList();
}
// Filter: Songs mit ALLEN aktiven Tags (AND-Logik)
return songs.where((s) {
if (s.tagIds == null) return false;
return aktiveTags.every((tagName) {
final tag = _tagsMap[tagName];
return tag != null && s.tagIds!.contains(tag.id);
});
}).toList();
}
Future<void> ladeSongs() async {
@@ -54,29 +101,22 @@ class MeloHomeViewModel extends ChangeNotifier {
notifyListeners();
try {
var alle = await db.alleSongs();
// Auto-Restore der Navidrome-Session beim Start
// Dummies bereinigen
await db.alteDummiesLoeschen();
if (alle.isEmpty) {
final beispiele = [
Song(titel: 'Leichtes Gepäck', kuenstler: 'Silbermond', album: 'Schritte',
dauerSekunden: 232, dateiPfad: '', groesseBytes: 5242880, istHeruntergeladen: true),
Song(titel: 'Auf uns', kuenstler: 'Andreas Bourani', album: 'Hey',
dauerSekunden: 241, dateiPfad: '', groesseBytes: 4819200, istHeruntergeladen: true),
Song(titel: '80 Millionen', kuenstler: 'Max Giesinger', album: 'Der Junge',
dauerSekunden: 225, dateiPfad: '', groesseBytes: 5107200, istHeruntergeladen: true),
Song(titel: 'Tage wie diese', kuenstler: 'Die Toten Hosen', album: 'Ballast',
dauerSekunden: 252, dateiPfad: '', groesseBytes: 4300800, istHeruntergeladen: true),
Song(titel: 'Atlantis', kuenstler: 'Frida Gold', album: 'Liebe',
dauerSekunden: 228, dateiPfad: '', groesseBytes: 3987200, istHeruntergeladen: true),
];
await db.songsEinfuegen(beispiele);
alle = await db.alleSongs();
await navidrome.ladeGespeicherteZugangsdaten();
if (navidrome.istVerbunden) {
ladeNavidromeAlben();
}
songs = alle;
// Echte Songs aus SQLite laden (ohne unspielbare Fake-Dummies)
songs = await db.alleSongs();
favoritenIds = await favoriten.favoritenIds();
letzteSongs = await db.letzteWiedergaben();
await ladeTags();
_starteCloudSyncScheduler();
} catch (e, stack) {
debugPrint('ladeSongs Fehler: $e\n$stack');
}
@@ -88,11 +128,12 @@ class MeloHomeViewModel extends ChangeNotifier {
Map<String, int> _berechneTagCounts() {
final counts = <String, int>{};
for (final s in songs) {
for (final t in tags) {
final name = t['name']!;
if (name == 'Alle') continue;
if (s.titel.contains(name) || s.kuenstler.contains(name)) {
counts[name] = (counts[name] ?? 0) + 1;
if (s.tagIds == null) continue;
for (final tagId in s.tagIds!) {
for (final entry in _tagsMap.entries) {
if (entry.value.id == tagId) {
counts[entry.key] = (counts[entry.key] ?? 0) + 1;
}
}
}
}
@@ -109,19 +150,37 @@ class MeloHomeViewModel extends ChangeNotifier {
}
dbTags = await db.alleTags();
}
tags = [{'name': 'Alle', 'icon': ''}, ...dbTags.map((t) => t.toDisplay())];
// Tag-Counts nachladen wenn Tags geladen
// Tags-Map füllen
_tagsMap = {for (final t in dbTags) t.name: t};
// Tag-IDs für jeden Song laden
for (final song in songs) {
if (song.id != null) {
final songTags = await db.tagsFuerSong(song.id!);
song.tagIds = songTags.map((t) => t.id!).toSet();
}
}
tags = _defaultTags;
tagCounts = _berechneTagCounts();
notifyListeners();
}
void spieleSong(Song song) {
player.setWarteschlange(songs,
startIndex: songs.indexWhere((s) => s.id == song.id));
player.spiele(song);
void spieleSong(Song song, {List<Song>? warteschlange}) {
player.setWarteschlange(warteschlange ?? songs,
startIndex: (warteschlange ?? songs).indexWhere((s) => s.id == song.id));
// Wiederaufnahme
final pos = (song.zuletztPosition != null && song.zuletztPosition! > 5)
? song.zuletztPosition! : 0;
player.spiele(song, position: pos);
// Position-Tracking starten (alle 10s speichern)
_startePositionTracking();
// In Verlauf speichern
if (song.id != null) {
db.positionAktualisieren(song.id!, 0);
db.positionAktualisieren(song.id!, song.zuletztPosition ?? 0);
}
// Play-Counter für Hidden Message
@@ -222,7 +281,43 @@ class MeloHomeViewModel extends ChangeNotifier {
@override
void dispose() {
_autoSyncTimer?.cancel();
_positionsSub?.cancel();
player.dispose();
super.dispose();
}
Future<void> _starteCloudSyncScheduler() async {
_autoSyncTimer?.cancel();
final prefs = await SharedPreferences.getInstance();
final autoSync = prefs.getBool('cloud_auto') ?? true;
final intervalStunden = prefs.getInt('cloud_interval') ?? 6;
if (!autoSync || intervalStunden <= 0) return;
_autoSyncTimer = Timer.periodic(Duration(hours: intervalStunden), (_) async {
try {
await cloud.restoreLogin();
if (!cloud.istAngemeldet) return;
final serverSongs = await cloud.listSongs();
if (serverSongs.isEmpty) return;
// Nur neue Songs herunterladen
final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music');
if (!await dir.exists()) await dir.create(recursive: true);
final localFiles = dir.listSync().whereType<File>()
.map((f) => f.path.split('/').last).toSet();
for (final s in serverSongs) {
final title = s['title'].toString();
if (localFiles.contains(title)) continue; // schon lokal
await cloud.download(s['id'].toString(), '${dir.path}/$title');
}
await ladeSongs(); // UI aktualisieren
} catch (e) {
MeloLogger().fehler('background_auto_sync', e);
}
});
}
}