111 lines
3.3 KiB
Dart
111 lines
3.3 KiB
Dart
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<bool>? _pendingReLogin;
|
|
|
|
MarianumConnectAuthInterceptor({
|
|
MarianumConnectTokenStorage tokenStorage =
|
|
const MarianumConnectTokenStorage(),
|
|
Dio? retryDio,
|
|
AuthLogin? loginClient,
|
|
}) : _tokenStorage = tokenStorage,
|
|
_retryDio = retryDio ?? Dio(),
|
|
_loginClient = loginClient ?? AuthLogin();
|
|
|
|
@override
|
|
Future<void> 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<void> 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<bool> _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<bool> _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<Response<dynamic>> _retryWithFreshToken(
|
|
RequestOptions originalOptions,
|
|
) async {
|
|
final freshToken = await _tokenStorage.readToken();
|
|
final headers = Map<String, dynamic>.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<dynamic>(clone);
|
|
}
|
|
}
|