139 lines
4.1 KiB
Dart
139 lines
4.1 KiB
Dart
import 'package:dio/dio.dart';
|
|
import '../storage/token_storage.dart';
|
|
|
|
class ApiInterceptor extends Interceptor {
|
|
final TokenStorage tokenStorage;
|
|
final Dio dio;
|
|
final Duration refreshFailureCooldown;
|
|
Future<bool> Function()? onTokenRefresh;
|
|
Future<void> Function()? onAuthFailure;
|
|
Future<bool>? _refreshFuture;
|
|
Future<void>? _authFailureFuture;
|
|
DateTime? _refreshBlockedUntil;
|
|
|
|
static const _retriedRequestKey = '_auth_retry_once';
|
|
static const _refreshPathSuffix = '/api/v1/auth/sessions/refresh';
|
|
|
|
ApiInterceptor({
|
|
required this.tokenStorage,
|
|
required this.dio,
|
|
this.refreshFailureCooldown = const Duration(seconds: 5),
|
|
this.onTokenRefresh,
|
|
});
|
|
|
|
@override
|
|
void onRequest(
|
|
RequestOptions options,
|
|
RequestInterceptorHandler handler,
|
|
) async {
|
|
final token = await tokenStorage.getAccessToken();
|
|
if (token != null) {
|
|
options.headers['Authorization'] = 'Bearer $token';
|
|
}
|
|
handler.next(options);
|
|
}
|
|
|
|
@override
|
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
|
final requestOptions = err.requestOptions;
|
|
final isUnauthorized = err.response?.statusCode == 401;
|
|
final shouldHandleUnauthorized =
|
|
isUnauthorized && _isAuthenticatedRequest(requestOptions);
|
|
|
|
if (err.response?.statusCode == 401 &&
|
|
onTokenRefresh != null &&
|
|
!_shouldSkipRefresh(requestOptions)) {
|
|
final refreshed = await _refreshTokenSingleflight();
|
|
if (refreshed) {
|
|
final token = await tokenStorage.getAccessToken();
|
|
if (token != null) {
|
|
final retryHeaders = Map<String, dynamic>.from(requestOptions.headers)
|
|
..['Authorization'] = 'Bearer $token';
|
|
final retryExtra = Map<String, dynamic>.from(requestOptions.extra)
|
|
..[_retriedRequestKey] = true;
|
|
final retryOptions = requestOptions.copyWith(
|
|
headers: retryHeaders,
|
|
extra: retryExtra,
|
|
);
|
|
try {
|
|
final response = await dio.fetch(retryOptions);
|
|
handler.resolve(response);
|
|
return;
|
|
} on DioException {
|
|
// Retry failed, proceed with original error.
|
|
}
|
|
}
|
|
} else if (shouldHandleUnauthorized) {
|
|
await _notifyAuthFailureSingleflight();
|
|
}
|
|
} else if (shouldHandleUnauthorized && _shouldSkipRefresh(requestOptions)) {
|
|
await _notifyAuthFailureSingleflight();
|
|
}
|
|
handler.next(err);
|
|
}
|
|
|
|
bool _isAuthenticatedRequest(RequestOptions options) {
|
|
return options.headers['Authorization'] != null;
|
|
}
|
|
|
|
Future<void> _notifyAuthFailureSingleflight() {
|
|
final existing = _authFailureFuture;
|
|
if (existing != null) {
|
|
return existing;
|
|
}
|
|
final callback = onAuthFailure;
|
|
if (callback == null) {
|
|
return Future<void>.value();
|
|
}
|
|
|
|
final future = callback().whenComplete(() {
|
|
_authFailureFuture = null;
|
|
});
|
|
_authFailureFuture = future;
|
|
return future;
|
|
}
|
|
|
|
bool _shouldSkipRefresh(RequestOptions options) {
|
|
final blockedUntil = _refreshBlockedUntil;
|
|
if (blockedUntil != null && DateTime.now().isBefore(blockedUntil)) {
|
|
return true;
|
|
}
|
|
return _normalizePath(options.path) == _refreshPathSuffix ||
|
|
options.extra[_retriedRequestKey] == true;
|
|
}
|
|
|
|
String _normalizePath(String rawPath) {
|
|
final parsed = Uri.tryParse(rawPath);
|
|
if (parsed == null) {
|
|
return rawPath.replaceFirst(RegExp(r'/+$'), '');
|
|
}
|
|
final normalized = parsed.path.replaceFirst(RegExp(r'/+$'), '');
|
|
return normalized.isEmpty ? '/' : normalized;
|
|
}
|
|
|
|
Future<bool> _refreshTokenSingleflight() {
|
|
final inflight = _refreshFuture;
|
|
if (inflight != null) {
|
|
return inflight;
|
|
}
|
|
final future = onTokenRefresh!().catchError((_) => false).whenComplete(() {
|
|
_refreshFuture = null;
|
|
});
|
|
_refreshFuture = future;
|
|
return future.then((refreshed) {
|
|
if (refreshed) {
|
|
_refreshBlockedUntil = null;
|
|
} else {
|
|
_refreshBlockedUntil = DateTime.now().add(refreshFailureCooldown);
|
|
}
|
|
return refreshed;
|
|
});
|
|
}
|
|
|
|
void reset() {
|
|
_refreshFuture = null;
|
|
_authFailureFuture = null;
|
|
_refreshBlockedUntil = null;
|
|
}
|
|
}
|