add refresh token

This commit is contained in:
Phuoc Nguyen
2025-10-21 16:30:11 +07:00
parent b94a19dd3f
commit 9c20a44a04
21 changed files with 246 additions and 67 deletions

View File

@@ -1,13 +1,16 @@
import 'package:dio/dio.dart';
import '../constants/api_constants.dart';
import '../storage/secure_storage.dart';
import 'api_interceptor.dart';
import 'refresh_token_interceptor.dart';
/// Dio HTTP client configuration
class DioClient {
late final Dio _dio;
String? _authToken;
final SecureStorage? secureStorage;
DioClient() {
DioClient({this.secureStorage}) {
_dio = Dio(
BaseOptions(
baseUrl: ApiConstants.fullBaseUrl,
@@ -34,6 +37,17 @@ class DioClient {
},
),
);
// Add refresh token interceptor (if secureStorage is provided)
if (secureStorage != null) {
_dio.interceptors.add(
RefreshTokenInterceptor(
dio: _dio,
secureStorage: secureStorage!,
),
);
print('🔧 DioClient: Refresh token interceptor added');
}
}
Dio get dio => _dio;

View File

@@ -0,0 +1,104 @@
import 'package:dio/dio.dart';
import '../constants/api_constants.dart';
import '../storage/secure_storage.dart';
/// Interceptor to handle automatic token refresh on 401 errors
class RefreshTokenInterceptor extends Interceptor {
final Dio dio;
final SecureStorage secureStorage;
// To prevent infinite loop of refresh attempts
bool _isRefreshing = false;
RefreshTokenInterceptor({
required this.dio,
required this.secureStorage,
});
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Check if error is 401 Unauthorized
if (err.response?.statusCode == 401) {
print('🔄 Interceptor: Got 401 error, attempting token refresh...');
// Avoid infinite refresh loop
if (_isRefreshing) {
print('❌ Interceptor: Already refreshing, skip');
return handler.next(err);
}
// Check if this is NOT the refresh token endpoint itself
final requestPath = err.requestOptions.path;
if (requestPath.contains('refresh')) {
print('❌ Interceptor: 401 on refresh endpoint, cannot retry');
// Clear tokens as refresh token is invalid
await secureStorage.deleteAllTokens();
return handler.next(err);
}
try {
_isRefreshing = true;
// Get refresh token from storage
final refreshToken = await secureStorage.getRefreshToken();
if (refreshToken == null) {
print('❌ Interceptor: No refresh token available');
await secureStorage.deleteAllTokens();
return handler.next(err);
}
print('🔄 Interceptor: Calling refresh token API...');
// Call refresh token API
final response = await dio.post(
ApiConstants.refreshToken,
data: {'refreshToken': refreshToken},
options: Options(
headers: {
// Don't include auth header for refresh request
ApiConstants.authorization: null,
},
),
);
if (response.statusCode == 200) {
// Extract new tokens from response
final responseData = response.data['data'] as Map<String, dynamic>;
final newAccessToken = responseData['access_token'] as String;
final newRefreshToken = responseData['refresh_token'] as String;
print('✅ Interceptor: Got new tokens, saving...');
// Save new tokens
await secureStorage.saveAccessToken(newAccessToken);
await secureStorage.saveRefreshToken(newRefreshToken);
// Update the failed request with new token
err.requestOptions.headers[ApiConstants.authorization] = 'Bearer $newAccessToken';
print('🔄 Interceptor: Retrying original request...');
// Retry the original request
final retryResponse = await dio.fetch(err.requestOptions);
print('✅ Interceptor: Original request succeeded after refresh');
_isRefreshing = false;
return handler.resolve(retryResponse);
} else {
print('❌ Interceptor: Refresh token API returned ${response.statusCode}');
await secureStorage.deleteAllTokens();
_isRefreshing = false;
return handler.next(err);
}
} catch (e) {
print('❌ Interceptor: Error during token refresh: $e');
await secureStorage.deleteAllTokens();
_isRefreshing = false;
return handler.next(err);
}
}
// Not a 401 error, pass through
return handler.next(err);
}
}

View File

@@ -7,10 +7,12 @@ part 'core_providers.g.dart';
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
/// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
@Riverpod(keepAlive: true)
DioClient dioClient(Ref ref) {
return DioClient();
final storage = ref.watch(secureStorageProvider);
return DioClient(secureStorage: storage);
}
/// Provider for SecureStorage (singleton)

View File

@@ -11,7 +11,8 @@ part of 'core_providers.dart';
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
/// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
@ProviderFor(dioClient)
const dioClientProvider = DioClientProvider._();
@@ -19,7 +20,8 @@ const dioClientProvider = DioClientProvider._();
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
/// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
final class DioClientProvider
extends $FunctionalProvider<DioClient, DioClient, DioClient>
@@ -27,7 +29,8 @@ final class DioClientProvider
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
/// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
const DioClientProvider._()
: super(
from: null,
@@ -61,7 +64,7 @@ final class DioClientProvider
}
}
String _$dioClientHash() => r'895f0dc2f8d5eab562ad65390e5c6d4a1f722b0d';
String _$dioClientHash() => r'a9edc35e0e918bfa8e6c4e3ecd72412fba383cb2';
/// Provider for SecureStorage (singleton)
///