Files
pyramid/lib/features/auth/login_notifier.dart
T
Bernd Steckmeister 891f3489bd fix: Soft-Logout darf nie mehr destruktiv enden (onSoftLogout-Guard)
Qualitäts-Review-Befund (b): Der Refresh-Token-Fix (9dc83f7) war richtig
analysiert, aber unvollständig – drei Restlücken gegen das matrix-SDK
6.2.0 verifiziert und geschlossen:

1. Client._handleSoftLogout ruft bei JEDEM Refresh-Fehler logout() auf,
   und logout() macht 'finally clear()' – die lokale Session inkl.
   Krypto-Schlüssel wird selbst bei einem transienten Netzwerkfehler
   zerstört. Mit refreshToken:true laufen Access-Tokens künftig ab,
   Refreshes werden Routine – das Restrisiko wäre also GESTIEGEN.
   Fix: eigener onSoftLogout-Handler (soft_logout_guard.dart) mit
   3 Versuchen, der nie wirft → SDK ruft nie logout() auf; bei
   Dauerfehler bleibt der Client soft-logged-out und der nächste
   Sync versucht es erneut (CLAUDE.md: Fehlerpfade dürfen NIE in
   einem stillen Logout enden).

2. Das Push-Hintergrund-Isolate (background_push.dart) teilt sich die
   pyramid.sqlite mit der Haupt-App; getEventByPushNotification()
   ruft ensureNotSoftLoggedOut() auf. Ohne Guard hätte ein
   Refresh-Fehler im Isolate (inkl. Token-Rotations-Race mit der
   Haupt-App) die Session der GESAMTEN App gelöscht. Guard auch dort
   gesetzt; das Race heilt der Retry, weil refreshAccessToken() den
   Token bei jedem Versuch frisch aus der DB liest.

3. isLoggedInProvider behandelte softLoggedOut wie ausgeloggt – die UI
   wäre bei jedem Routine-Token-Refresh kurz auf den Login-Screen
   gesprungen (für Nutzer nicht von einem 'Random Logout' zu
   unterscheiden). Jetzt zählt nur echtes loggedOut.

UNGETESTET (Pi) – gehört zum selben ausstehenden PC-Praxistest wie
9dc83f7: Login → Nachrichten → Logout → Neu-Login → alte Nachrichten
lesbar; zusätzlich auth_log.txt auf die neuen Guard-Einträge prüfen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:42:15 +02:00

121 lines
3.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:matrix/matrix.dart' as mx;
import 'package:pyramid/core/fcm_push_service.dart';
import 'package:pyramid/core/matrix_client.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _prefHomeserver = 'homeserver_url';
final homeserverProvider = StateProvider<String?>((ref) => null);
Future<void> initHomeserver(WidgetRef ref) async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_prefHomeserver);
ref.read(homeserverProvider.notifier).state = saved;
}
Future<void> saveHomeserver(WidgetRef ref, String url) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefHomeserver, url);
ref.read(homeserverProvider.notifier).state = url;
}
enum LoginStatus { idle, loading, error }
class LoginFormState {
final LoginStatus status;
final String? errorMessage;
const LoginFormState({this.status = LoginStatus.idle, this.errorMessage});
LoginFormState copyWith({LoginStatus? status, String? errorMessage}) =>
LoginFormState(
status: status ?? this.status,
errorMessage: errorMessage,
);
}
class LoginNotifier extends StateNotifier<LoginFormState> {
final Ref _ref;
LoginNotifier(this._ref) : super(const LoginFormState());
Future<void> login({
required String username,
required String password,
required String homeserver,
String? deviceName,
}) async {
state = state.copyWith(status: LoginStatus.loading, errorMessage: null);
final client = _ref.read(matrixClientProvider).valueOrNull;
if (client == null) {
state = state.copyWith(
status: LoginStatus.error,
errorMessage: 'Client nicht initialisiert',
);
return;
}
if (client.isLogged()) {
state = state.copyWith(status: LoginStatus.idle);
return;
}
try {
final serverUri = Uri.https(
homeserver.replaceAll(RegExp(r'^https?://'), ''),
'',
);
await client.checkHomeserver(serverUri);
await client.login(
mx.LoginType.mLoginPassword,
identifier: mx.AuthenticationUserIdentifier(user: username),
password: password,
initialDeviceDisplayName: deviceName ?? 'Pyramid',
// Ohne Refresh-Token eskaliert das SDK jeden Soft-Logout (z. B. nach
// Ablauf des Access-Tokens) sofort zu einem echten, destruktiven
// Logout, weil `refreshAccessToken()` sonst mangels Refresh-Token
// fehlschlägt (siehe matrix-Paket Client._handleSoftLogout). Das ist
// der wahrscheinlichste Auslöser des Uta-Random-Logout-Bugs.
refreshToken: true,
);
if (Platform.isAndroid) unawaited(initFcm(client));
state = state.copyWith(status: LoginStatus.idle);
} on mx.MatrixException catch (e) {
state = state.copyWith(
status: LoginStatus.error,
errorMessage: e.errorMessage,
);
} catch (e) {
state = state.copyWith(
status: LoginStatus.error,
errorMessage: e.toString(),
);
}
}
}
final loginNotifierProvider =
StateNotifierProvider<LoginNotifier, LoginFormState>((ref) {
return LoginNotifier(ref);
});
final isLoggedInProvider = StreamProvider<bool>((ref) async* {
final client = await ref.watch(matrixClientProvider.future);
yield client.isLogged();
await for (final loginState in client.onLoginStateChanged.stream) {
// softLoggedOut ist KEIN Logout: Die Session existiert lokal weiter, das
// SDK versucht gerade einen stillen Token-Refresh. Würde hier `false`
// geliefert, spränge die UI bei jedem Routine-Refresh kurz auf den
// Login-Screen für Nutzer nicht von einem „Random Logout" zu
// unterscheiden. Nur ein echtes loggedOut zählt.
yield loginState != mx.LoginState.loggedOut;
}
});