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() ?? '', ); } Future clear() async { await _storage.delete(key: _tokenKey); await _storage.delete(key: _tokenIdKey); await _storage.delete(key: _expiresAtKey); } }