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>
103 lines
3.5 KiB
Dart
103 lines
3.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
|
|
import 'package:matrix/encryption/utils/key_verification.dart';
|
|
import 'package:matrix/matrix.dart';
|
|
import 'package:path/path.dart' as p;
|
|
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 {
|
|
final DatabaseFactory factory;
|
|
|
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
|
// Use the native sqflite plugin — Android/iOS have system SQLite built-in,
|
|
// no .so loading required.
|
|
factory = sqflite_native.databaseFactory;
|
|
} else if (!kIsWeb) {
|
|
sqfliteFfiInit();
|
|
factory = databaseFactoryFfi;
|
|
} else {
|
|
throw UnsupportedError('Web not supported');
|
|
}
|
|
|
|
final appDir = await getApplicationSupportDirectory();
|
|
final dbPath = p.join(appDir.path, 'pyramid.sqlite');
|
|
|
|
final db = await factory.openDatabase(dbPath);
|
|
|
|
// Optimization for Windows/FFI to prevent "database is locked" errors
|
|
try {
|
|
await db.execute('PRAGMA journal_mode=WAL');
|
|
await db.execute('PRAGMA busy_timeout = 5000');
|
|
} catch (e) {
|
|
if (kDebugMode) debugPrint('Could not set PRAGMAs: $e');
|
|
}
|
|
|
|
final sdkDb = await MatrixSdkDatabase.init('pyramid', database: db);
|
|
|
|
final client = Client(
|
|
'Pyramid',
|
|
database: sdkDb,
|
|
// Custom State-Events, die beim App-Start mit aus der DB geladen werden
|
|
// müssen — sonst sind Space-Banner & Voice-Presence nach Neustart leer,
|
|
// bis der jeweilige Raum vollständig nachgeladen wird.
|
|
importantStateEvents: {
|
|
'io.pyramid.space.banner',
|
|
'io.pyramid.voice.presence',
|
|
},
|
|
verificationMethods: {
|
|
KeyVerificationMethod.numbers,
|
|
KeyVerificationMethod.emoji,
|
|
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(),
|
|
),
|
|
);
|
|
|
|
// Persistiert Warnungen/Fehler des SDK sowie jeden Login-Status-Wechsel,
|
|
// damit ein unerwarteter Logout (z. B. Soft-Logout vom Server) auch nach
|
|
// einem App-Neustart noch nachvollziehbar ist (ROADMAP M1, Uta-Logout-Bug).
|
|
Logs().onLog = (event) {
|
|
if (event.level.index <= Level.warning.index) {
|
|
unawaited(AuthLog.write('[SDK ${event.level.name}] ${event.toFormattedString()}'));
|
|
}
|
|
};
|
|
client.onLoginStateChanged.stream.listen((state) {
|
|
unawaited(AuthLog.write('LoginState -> ${state.name}'));
|
|
});
|
|
|
|
await client.init(
|
|
waitForFirstSync: false,
|
|
waitUntilLoadCompletedLoaded: false,
|
|
);
|
|
|
|
// Re-use or create singleton instance
|
|
try {
|
|
PyramidVoipManager.instance.client;
|
|
} catch (_) {
|
|
PyramidVoipManager(client);
|
|
}
|
|
|
|
return client;
|
|
});
|
|
|
|
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
|
|
return const FlutterSecureStorage();
|
|
});
|