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);
}
}