fix: session restore timeout + block .git access

- Add 12s timeout to restoreSession() — client.init() could hang
  indefinitely if token expired or server unreachable, leaving user
  on spinner forever. Now clears stale credentials and shows login.
- Sync persistence startup wrapped in try-catch — non-fatal
- .htaccess blocks .git directory (was returning 200 to scanners)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:55:27 +10:00
parent f12a7ac1fd
commit 96550c3411

View File

@@ -1,7 +1,9 @@
// Version: 1.1.0 | Created: 2026-04-01 // Version: 1.1.1 | Created: 2026-04-01
// Riverpod notifier that owns the auth state machine. // Riverpod notifier that owns the auth state machine.
// All login/logout/session-restore transitions go through here. // All login/logout/session-restore transitions go through here.
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../features/auth/data/auth_repository.dart'; import '../../features/auth/data/auth_repository.dart';
@@ -39,25 +41,40 @@ class AuthNotifier extends _$AuthNotifier {
try { try {
final repo = ref.read(authRepositoryProvider); final repo = ref.read(authRepositoryProvider);
await repo.restoreSession(
// Timeout: client.init() starts the Matrix sync loop and can hang
// indefinitely if the token is expired or the server is unreachable.
// After 12 seconds we give up and send the user to the login screen.
await repo
.restoreSession(
accessToken: credentials.accessToken, accessToken: credentials.accessToken,
userId: credentials.userId, userId: credentials.userId,
deviceId: credentials.deviceId, deviceId: credentials.deviceId,
)
.timeout(
const Duration(seconds: 12),
onTimeout: () => throw Exception('Session restore timed out'),
); );
state = AuthState.authenticated( state = AuthState.authenticated(
userId: credentials.userId, userId: credentials.userId,
accessToken: credentials.accessToken, accessToken: credentials.accessToken,
deviceId: credentials.deviceId, deviceId: credentials.deviceId,
); );
// Resume background persistence for the restored session. // Resume background persistence — fire-and-forget so it never blocks auth.
try {
ref.read(syncPersistenceServiceProvider).start(); ref.read(syncPersistenceServiceProvider).start();
} catch (_) {
// Persistence failure is non-fatal; the app works without it.
}
} on AuthFailure { } on AuthFailure {
// Stored credentials are invalid; force re-login. // Stored credentials are invalid; force re-login.
await storage.clearCredentials(); await storage.clearCredentials();
state = const AuthState.unauthenticated(); state = const AuthState.unauthenticated();
} on Exception { } on Exception {
// Network offline at startup; still land on login rather than crashing. // Covers: timeout, network offline, Olm init failure.
// Clear stale credentials so the login screen appears cleanly.
await storage.clearCredentials(); await storage.clearCredentials();
state = const AuthState.unauthenticated(); state = const AuthState.unauthenticated();
} }