diff --git a/CHANGES.md b/CHANGES.md index 85efe3e..b6e4be4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1173,3 +1173,17 @@ 2026-07-03 12:57 [Write] /home/steggi/pyramid/lib/features/chat/document_viewer.dart 2026-07-03 12:58 [Edit] /home/steggi/pyramid/ROADMAP.md 2026-07-03 12:58 [Edit] /home/steggi/pyramid/PROGRESS.md +2026-07-03 14:33 [Edit] /home/steggi/pyramid/lib/widgets/settings/settings_voice.dart +2026-07-03 14:36 [Edit] /home/steggi/pyramid/lib/features/chat/document/document_viewer_pdf.dart +2026-07-03 14:36 [Edit] /home/steggi/pyramid/lib/features/chat/document/document_viewer_pdf.dart +2026-07-03 14:36 [Edit] /home/steggi/pyramid/lib/features/chat/document/document_viewer_pdf.dart +2026-07-03 14:36 [Edit] /home/steggi/pyramid/lib/features/chat/document/document_viewer_archive.dart +2026-07-03 14:36 [Edit] /home/steggi/pyramid/lib/features/chat/document/document_viewer_archive.dart +2026-07-03 14:36 [Edit] /home/steggi/pyramid/lib/widgets/settings/settings_encryption.dart +2026-07-03 14:36 [Edit] /home/steggi/pyramid/lib/features/chat/message/message_text.dart +2026-07-03 14:41 [Write] /home/steggi/pyramid/lib/core/soft_logout_guard.dart +2026-07-03 14:41 [Edit] /home/steggi/pyramid/lib/core/matrix_client.dart +2026-07-03 14:41 [Edit] /home/steggi/pyramid/lib/core/matrix_client.dart +2026-07-03 14:41 [Edit] /home/steggi/pyramid/lib/core/background_push.dart +2026-07-03 14:41 [Edit] /home/steggi/pyramid/lib/core/background_push.dart +2026-07-03 14:41 [Edit] /home/steggi/pyramid/lib/features/auth/login_notifier.dart diff --git a/lib/core/background_push.dart b/lib/core/background_push.dart index d7b28c0..6db03a5 100644 --- a/lib/core/background_push.dart +++ b/lib/core/background_push.dart @@ -9,6 +9,7 @@ import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:matrix/matrix.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import 'package:pyramid/core/soft_logout_guard.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite/sqflite.dart' as sqflite_native; @@ -201,6 +202,12 @@ Future _buildClient() async { database: sdkDb, nativeImplementations: NativeImplementationsDummy(), logLevel: Level.error, + // Kritisch: getEventByPushNotification() ruft als Erstes + // ensureNotSoftLoggedOut() auf. Ohne eigenen Handler würde ein + // fehlgeschlagener Token-Refresh AUS DEM HINTERGRUND-ISOLATE heraus + // logout()+clear() auf der geteilten pyramid.sqlite auslösen und die + // Session der gesamten App zerstören. Siehe soft_logout_guard.dart. + onSoftLogout: guardedSoftLogoutRefresh, ); await client diff --git a/lib/core/matrix_client.dart b/lib/core/matrix_client.dart index 4f0f8bc..2b5fcab 100644 --- a/lib/core/matrix_client.dart +++ b/lib/core/matrix_client.dart @@ -12,6 +12,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart' as sqflite_native; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:pyramid/core/auth_log.dart'; +import 'package:pyramid/core/soft_logout_guard.dart'; import 'package:pyramid/core/voip_manager.dart'; final matrixClientProvider = FutureProvider((ref) async { @@ -59,6 +60,10 @@ final matrixClientProvider = FutureProvider((ref) async { KeyVerificationMethod.qrShow, }, logLevel: kReleaseMode ? Level.warning : Level.verbose, + // Ohne eigenen Handler eskaliert das SDK jeden fehlgeschlagenen + // Token-Refresh (auch bloße Netzwerkfehler!) zu logout()+clear() — + // Session und Krypto-Schlüssel wären weg. Siehe soft_logout_guard.dart. + onSoftLogout: guardedSoftLogoutRefresh, nativeImplementations: NativeImplementationsIsolate( compute, vodozemacInit: () => vod.init(), diff --git a/lib/core/soft_logout_guard.dart b/lib/core/soft_logout_guard.dart new file mode 100644 index 0000000..85ff6fb --- /dev/null +++ b/lib/core/soft_logout_guard.dart @@ -0,0 +1,41 @@ +import 'package:matrix/matrix.dart'; +import 'package:pyramid/core/auth_log.dart'; + +/// Wendet einen Soft-Logout durch Token-Refresh ab, ohne JE in einen +/// destruktiven Logout zu eskalieren. +/// +/// Hintergrund: Ohne gesetztes `Client.onSoftLogout` behandelt das matrix-SDK +/// einen fehlgeschlagenen Refresh so: catch → `logout()` → `finally clear()` — +/// die lokale Session inkl. Krypto-Schlüssel wird also selbst dann zerstört, +/// wenn der Refresh nur an einem transienten Netzwerkfehler scheiterte +/// (Gerät offline im Moment des Token-Ablaufs) oder am Rotations-Race mit dem +/// Push-Hintergrund-Isolate (beide teilen sich die DB; refreshAccessToken() +/// liest den Refresh-Token bei jedem Versuch frisch aus der DB, deshalb heilt +/// ein Retry dieses Race). Wirft dieser Handler nicht, ruft das SDK auch kein +/// `logout()` auf (Client._handleSoftLogout, matrix 6.2.0). +/// +/// Regel aus CLAUDE.md: Fehlerpfade dürfen NIE in einem stillen Logout enden. +/// Schlägt der Refresh dauerhaft fehl, bleibt der Client im Zustand +/// soft-logged-out; der nächste Sync/API-Aufruf stößt den Refresh erneut an. +Future guardedSoftLogoutRefresh(Client client) async { + for (var attempt = 1; attempt <= 3; attempt++) { + try { + await client.refreshAccessToken(); + await AuthLog.write( + 'Soft-Logout durch Token-Refresh abgewendet (Versuch $attempt)', + ); + return; + } catch (e) { + await AuthLog.write( + 'Soft-Logout: Token-Refresh fehlgeschlagen (Versuch $attempt/3): $e', + ); + if (attempt < 3) { + await Future.delayed(Duration(seconds: 2 * attempt)); + } + } + } + await AuthLog.write( + 'Soft-Logout: Refresh dauerhaft fehlgeschlagen – Session wird NICHT ' + 'zerstört, nächster Sync/API-Aufruf versucht es erneut.', + ); +} diff --git a/lib/features/auth/login_notifier.dart b/lib/features/auth/login_notifier.dart index 34dbc38..d3845de 100644 --- a/lib/features/auth/login_notifier.dart +++ b/lib/features/auth/login_notifier.dart @@ -110,6 +110,11 @@ final isLoggedInProvider = StreamProvider((ref) async* { final client = await ref.watch(matrixClientProvider.future); yield client.isLogged(); await for (final loginState in client.onLoginStateChanged.stream) { - yield loginState == mx.LoginState.loggedIn; + // 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; } });