import 'dart:async'; import 'dart:developer'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../model/account_data.dart'; import 'auth/login/login.dart'; import 'auth/login/login_request.dart'; import 'auth/login/login_response.dart'; import 'errors/connect_exception.dart'; /// Holds the Bearer token issued by `POST /auth/login` so that subsequent /// RMV calls can attach it without prompting the user. Uses the LDAP /// credentials already kept in [AccountData], so this is transparent to the /// user — no extra login UI. class ConnectAuthStore { static const _tokenKey = 'connect_bearer_token'; static const _expiresAtKey = 'connect_token_expires_at'; static const _tokenName = 'MarianumMobile App'; static const _expiryGuard = Duration(minutes: 1); static final ConnectAuthStore _instance = ConnectAuthStore._(); factory ConnectAuthStore() => _instance; static ConnectAuthStore get instance => _instance; ConnectAuthStore._(); final FlutterSecureStorage _storage = const FlutterSecureStorage(); String? _token; DateTime? _expiresAt; bool _hydrated = false; Future? _inflightLogin; Future _hydrate() async { if (_hydrated) return; _token = await _storage.read(key: _tokenKey); final rawExp = await _storage.read(key: _expiresAtKey); _expiresAt = rawExp == null ? null : DateTime.tryParse(rawExp); _hydrated = true; } bool _isUsable() { if (_token == null || _token!.isEmpty) return false; final exp = _expiresAt; if (exp == null) return true; return DateTime.now().add(_expiryGuard).isBefore(exp); } /// Returns a usable bearer token, logging in if necessary. Concurrent /// callers share the same in-flight login future so a single 401 doesn't /// trigger N parallel logins. Future getToken({bool forceRefresh = false}) async { await _hydrate(); if (!forceRefresh && _isUsable()) return _token!; return _inflightLogin ??= _login().whenComplete(() { _inflightLogin = null; }); } Future _login() async { if (!AccountData().isPopulated()) { throw ConnectException.notAuthenticated(); } final username = AccountData().getUsername(); final password = AccountData().getPassword(); final LoginResponse response; try { response = await Login( LoginRequest( username: username, password: password, tokenName: _tokenName, ), ).run(); } on ConnectException { rethrow; } catch (e, st) { log('connect login threw: $e', stackTrace: st); throw ConnectException( userMessage: 'Anmeldung am Connect-Server fehlgeschlagen. Bitte später erneut versuchen.', technicalDetails: 'connect login failed: $e', ); } _token = response.token; _expiresAt = response.expiresAt; await _storage.write(key: _tokenKey, value: response.token); if (response.expiresAt != null) { await _storage.write( key: _expiresAtKey, value: response.expiresAt!.toIso8601String(), ); } else { await _storage.delete(key: _expiresAtKey); } return response.token; } Future invalidate() async { _token = null; _expiresAt = null; await _storage.delete(key: _tokenKey); await _storage.delete(key: _expiresAtKey); } /// Same as [invalidate] — separate method to make logout call-sites read /// clearly. Future clear() => invalidate(); }