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 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>
This commit is contained in:
@@ -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<Client?> _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
|
||||
|
||||
@@ -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<Client>((ref) async {
|
||||
@@ -59,6 +60,10 @@ final matrixClientProvider = FutureProvider<Client>((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(),
|
||||
|
||||
@@ -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<void> 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.',
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user