import 'package:dio/dio.dart'; import '../../../model/account_data.dart'; import '../queries/auth_login/auth_login.dart'; import 'device_token_name.dart'; import 'token_storage.dart'; /// Adds the bearer token to outgoing Marianum-Connect requests and, on 401, /// re-logs in once with the credentials in [AccountData] before retrying. class MarianumConnectAuthInterceptor extends Interceptor { static const _retriedKey = 'mc_auth_retried'; final MarianumConnectTokenStorage _tokenStorage; final Dio _retryDio; final AuthLogin _loginClient; // Single-flight lock: parallel 401s share the same login Future instead of // each spawning a fresh row in api_tokens. Future? _pendingReLogin; MarianumConnectAuthInterceptor({ MarianumConnectTokenStorage tokenStorage = const MarianumConnectTokenStorage(), Dio? retryDio, AuthLogin? loginClient, }) : _tokenStorage = tokenStorage, _retryDio = retryDio ?? Dio(), _loginClient = loginClient ?? AuthLogin(); @override Future onRequest( RequestOptions options, RequestInterceptorHandler handler, ) async { // Wait for an in-flight re-login so nachrückende Requests den frischen // Token mitschicken statt ein eigenes 401 einzufangen. final pending = _pendingReLogin; if (pending != null) await pending; final token = await _tokenStorage.readToken(); if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); } @override Future onError( DioException err, ErrorInterceptorHandler handler, ) async { final response = err.response; if (response?.statusCode != 401 || err.requestOptions.extra[_retriedKey] == true) { handler.next(err); return; } final refreshed = await _attemptReLogin(); if (!refreshed) { handler.next(err); return; } try { final retried = await _retryWithFreshToken(err.requestOptions); handler.resolve(retried); } on DioException catch (retryError) { handler.next(retryError); } } Future _attemptReLogin() { final inFlight = _pendingReLogin; if (inFlight != null) return inFlight; final fresh = _performReLogin(); _pendingReLogin = fresh; fresh.whenComplete(() { if (identical(_pendingReLogin, fresh)) _pendingReLogin = null; }); return fresh; } Future _performReLogin() async { if (!AccountData().isPopulated()) return false; try { await _loginClient.run( username: AccountData().getUsername(), password: AccountData().getPassword(), tokenName: await DeviceTokenName.resolve(), ); return true; } catch (_) { await _tokenStorage.clear(); return false; } } Future> _retryWithFreshToken( RequestOptions originalOptions, ) async { final freshToken = await _tokenStorage.readToken(); final headers = Map.of(originalOptions.headers); if (freshToken != null && freshToken.isNotEmpty) { headers['Authorization'] = 'Bearer $freshToken'; } final clone = originalOptions.copyWith( headers: headers, extra: {...originalOptions.extra, _retriedKey: true}, ); return _retryDio.fetch(clone); } }