Files
plezy/lib/services/seerr/seerr_session_store.dart
T
edde746 66549e3a67 fix(prefs): route every credential read through the tolerant path
The wrong-type recovery only covered reads that went through a
BaseSharedPreferencesService instance. The three stores that hold
credentials read the shared cache directly, so a mistyped value there
still threw a raw TypeError or, for Seerr, was swallowed by a catch-all
and reported as "no session" — the registry documented protection it did
not actually provide.

readPreferenceTolerantly now takes the cache, so CredentialVault,
TrackerAccountStore and SeerrSessionStore get the same classification as
the settings layer. CredentialVault's post-write re-read moves outside
its catch: a wrong-typed value written by another isolate was swallowed
there, and the process then returned a key that never durably landed,
making every ciphertext written under it unreadable on the next launch.

Those stores are consulted long after startup, where a throw is an
unhandled provider error rather than a repair prompt, so SettingsService
initialization now walks the cached key set once and reads every
sensitive key. That puts the failure inside a fatal gate step while the
store is still open and a surgical single-key repair is possible.

The remaining direct reads in settings and storage are routed too; the
only ones left are the library-density dual-type migration, which probes
both types deliberately, and an untyped switch that is type-safe by
construction.
2026-07-31 21:45:32 +02:00

47 lines
1.8 KiB
Dart

import '../../models/seerr/seerr_session.dart';
import '../../profiles/profile.dart';
import '../base_shared_preferences_service.dart';
import '../credential_vault.dart';
/// Per-Plex-profile persistence for the Seerr session, mirroring
/// `TrackerAccountStore`'s `user_{uuid}_{baseKey}` scoping.
///
/// The password ([SeerrSession.secret]) is CredentialVault-protected at the
/// store boundary; a failed decrypt degrades to an empty secret (the session
/// keeps working until its cookie expires) rather than dropping the session.
class SeerrSessionStore {
static const String _baseKey = 'seerr_session';
const SeerrSessionStore();
String _scopedKey(String userUuid) => profileScopedPrefsKey(userUuid, _baseKey);
Future<SeerrSession?> load(String userUuid) async {
final prefs = await BaseSharedPreferencesService.sharedCache();
// Outside the try below on purpose: an unreadable credential must
// reach the repair prompt, not be swallowed as 'no session'.
final raw = readTolerantString(prefs, _scopedKey(userUuid));
if (raw == null) return null;
try {
final session = SeerrSession.decode(raw);
if (session.secret.isEmpty) return session;
return session.copyWith(secret: await CredentialVault.reveal(session.secret) ?? '');
} catch (_) {
return null;
}
}
Future<void> save(String userUuid, SeerrSession session) async {
final prefs = await BaseSharedPreferencesService.sharedCache();
final protected = session.secret.isEmpty
? session
: session.copyWith(secret: await CredentialVault.protect(session.secret));
await prefs.setString(_scopedKey(userUuid), protected.encode());
}
Future<void> clear(String userUuid) async {
final prefs = await BaseSharedPreferencesService.sharedCache();
await prefs.remove(_scopedKey(userUuid));
}
}