Login-Dialog zeigt echte Server-Fehlermeldung + pwLen im Log

Tiefe Debug-Runde bewiesen: Server, DB-Passwort und App-Token-Logik sind
korrekt (8/8-Repro gegen Live-Server) — abgelehnt wird ein falscher
Passwort-Wert vom Gerät. Damit das künftig sofort sichtbar ist:
- NavidromeService.letzterLoginFehler hält die Server-Meldung fest
- Login-Dialog zeigt sie statt generischem "Login fehlgeschlagen"
- Fehler-Log enthält die Passwort-Länge (nie das Passwort selbst)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRj3pupcigUi683vy2Wwnz
This commit is contained in:
Hermes (Server)
2026-08-20 16:13:34 +02:00
co-authored by Claude Fable 5
parent 0776177901
commit 5d08da1086
4 changed files with 79 additions and 2 deletions
+18
View File
@@ -5,6 +5,24 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
## [Unreleased]
### 🔎 Login-Dialog zeigt jetzt die echte Server-Fehlermeldung (2026-08-20)
-**Was:** Schlägt der Navidrome-Login fehl, zeigt der Dialog jetzt die
Original-Meldung des Servers (z. B. „Wrong username or password") statt
nur „Login fehlgeschlagen". So ist sofort klar, ob das Passwort falsch ist
oder ein Netzwerkproblem vorliegt.
- 🔧 **Details:**
- `NavidromeService.letzterLoginFehler` hält die Server-Meldung des letzten
abgelehnten Logins fest (wird bei jedem Ping zurückgesetzt).
- Das Fehler-Log enthält jetzt die Passwort-**Länge** (`pwLen=…`, nie das
Passwort selbst) — ein Tippfehler wird so im Log sofort sichtbar
(`Melo…!` hat 11 Zeichen).
- 🐛 **Hintergrund (tiefe Debug-Runde):** Server-seitig ist alles nachweislich
korrekt — Subsonic-API (`/rest/*`) läuft trotz SSO-Umbau ohne Forward-Auth,
DB-Passwort für „Baka" stimmt, und die exakte App-Token-Logik (Base64-Salt +
md5) wurde 8/8-mal erfolgreich gegen den Live-Server reproduziert. Die
Ablehnungen kommen von einem falschen Passwort-Wert auf dem Gerät; die neue
Fehlermeldung macht genau das künftig direkt sichtbar.
### 🔎 Login-Diagnose: Passwort-Sichtbarkeit + Trim (2026-08-20)
- 🐛 **„Login fehlgeschlagen" trotz korrekter Zugangsdaten:** Systematische
Analyse (App-Code, App-Logs, Caddy/Navidrome-Logs, 1:1-Nachbau des
+6
View File
@@ -77,6 +77,10 @@ class NavidromeService {
String _salt = '';
String? _token;
/// Server-Meldung des letzten abgelehnten Logins (z. B. „Wrong username
/// or password"), damit der Login-Dialog die echte Ursache zeigen kann.
String? letzterLoginFehler;
static const _version = '1.16.1';
static const _client = 'Melo';
@@ -179,6 +183,7 @@ class NavidromeService {
}
Future<bool> ping() async {
letzterLoginFehler = null;
try {
final r = await http.get(_uri('ping.view')).timeout(const Duration(seconds: 10));
if (r.statusCode != 200) {
@@ -191,6 +196,7 @@ class NavidromeService {
logger.info('✅ Navidrome Verbindung erfolgreich: $_serverUrl');
return true;
} on NavidromeException catch (e) {
letzterLoginFehler = e.message;
logger.warning('Navidrome Login abgelehnt: ${e.message}');
return false;
} catch (e) {
+7 -2
View File
@@ -288,10 +288,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (ctx.mounted) Navigator.pop(ctx);
if (mounted) setState(() {});
} else if (ctx.mounted) {
// Server-Meldung + Passwortlänge festhalten: „Wrong
// username or password" bei pwLen=10 zeigt sofort
// einen Tippfehler statt eines Netzwerkproblems.
final grund = _navidrome.letzterLoginFehler;
await logger.error(
'Navidrome Login fehlgeschlagen (pwLeer=${passCtrl.text.isEmpty})');
'Navidrome Login fehlgeschlagen '
'(pwLen=${pass.length}, grund=${grund ?? 'unbekannt'})');
setDialogState(() =>
fehler = '❌ Login fehlgeschlagen');
fehler = '${grund ?? 'Login fehlgeschlagen'}');
}
},
child: const Text('Login',
+48
View File
@@ -0,0 +1,48 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:melo/services/logger_service.dart';
import 'package:melo/services/navidrome_service.dart';
/// Tests für die Login-Fehlerdiagnose: Lehnt der Server den Login ab
/// (status:failed, z. B. „Wrong username or password"), muss die App die
/// Server-Meldung festhalten, damit der Login-Dialog sie anzeigen kann —
/// ein generisches „Login fehlgeschlagen" verschleiert die Ursache.
void main() {
late HttpServer server;
setUpAll(() async => logger.init());
Future<Uri> starteServer(String body) async {
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.listen((req) {
req.response
..statusCode = 200
..headers.contentType = ContentType.json
..write(body);
req.response.close();
});
return Uri.parse('http://127.0.0.1:${server.port}');
}
tearDown(() async => server.close(force: true));
test('ping speichert Server-Meldung bei abgelehntem Login', () async {
final url = await starteServer(
'{"subsonic-response":{"status":"failed","version":"1.16.1",'
'"error":{"code":40,"message":"Wrong username or password"}}}');
final svc = NavidromeService();
svc.setCredentials(url.toString(), 'Baka', 'falsch');
expect(await svc.ping(), isFalse);
expect(svc.letzterLoginFehler, 'Wrong username or password');
});
test('ping löscht alte Fehlermeldung bei Erfolg', () async {
final url = await starteServer(
'{"subsonic-response":{"status":"ok","version":"1.16.1"}}');
final svc = NavidromeService();
svc.setCredentials(url.toString(), 'Baka', 'richtig');
expect(await svc.ping(), isTrue);
expect(svc.letzterLoginFehler, isNull);
});
}