import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../errors/auth_exception.dart'; /// `first_unlock` accessibility so the token can be read during background /// requests (telemetry heartbeat, push-triggered syncs) after the first device /// unlock following a reboot. The keychain default (`whenUnlocked`) throws /// `-25308 errSecInteractionNotAllowed` when the device is locked. const IOSOptions _mcIosOptions = IOSOptions( accessibility: KeychainAccessibility.first_unlock, ); /// Persists the Marianum-Connect bearer token in the platform keystore. Kept /// separate from `SessionManager` because the username/password live on (Nextcloud /// + MHSL still need them) while the MC token is short-lived and per-endpoint. class MarianumConnectTokenStorage { static const _tokenKey = 'mc_bearer_token'; static const _tokenIdKey = 'mc_token_id'; static const _expiresAtKey = 'mc_token_expires_at'; final FlutterSecureStorage _storage; const MarianumConnectTokenStorage([ this._storage = const FlutterSecureStorage(iOptions: _mcIosOptions), ]); Future readToken() => _storage.read(key: _tokenKey); /// Request options carrying the stored token, for probes that bypass the /// auth interceptor. Throws [AuthException] when no token is stored. Future requireBearerOptions(String caller) async { final token = await readToken(); if (token == null || token.isEmpty) { throw AuthException.unauthorized( technicalDetails: '$caller: no bearer token in storage', ); } return Options(headers: {'Authorization': 'Bearer $token'}); } Future readTokenId() => _storage.read(key: _tokenIdKey); Future readExpiresAt() async { final raw = await _storage.read(key: _expiresAtKey); if (raw == null || raw.isEmpty) return null; return DateTime.tryParse(raw); } Future write({ required String token, required String tokenId, required DateTime? expiresAt, }) async { await _storage.write(key: _tokenKey, value: token); await _storage.write(key: _tokenIdKey, value: tokenId); await _storage.write( key: _expiresAtKey, value: expiresAt?.toIso8601String() ?? '', ); } static const bearerKey = _tokenKey; static const fieldKeys = [_tokenKey, _tokenIdKey, _expiresAtKey]; /// Raw stored fields, for parking the token of an inactive account. Future> readAll() async => { for (final key in fieldKeys) key: ?await _storage.read(key: key), }; /// Restores fields from [readAll]; missing ones are deleted. Future writeAll(Map fields) async { for (final key in fieldKeys) { final value = fields[key]; if (value == null) { await _storage.delete(key: key); } else { await _storage.write(key: key, value: value); } } } Future clear() async { await _storage.delete(key: _tokenKey); await _storage.delete(key: _tokenIdKey); await _storage.delete(key: _expiresAtKey); } }