891f3489bd
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 wie9dc83f7: 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>
121 lines
3.8 KiB
Dart
121 lines
3.8 KiB
Dart
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;
|
||
}
|
||
});
|