migrated timetable integration from WebUntis to the MarianumConnect API, implementing a Dio-based client with bearer token authentication, background session validation, and auto-refresh logic.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user