Refactor network request handling by introducing AuthInterceptor for improved authentication management. Removed the previous request interceptor logic to streamline the code and enhance maintainability.

This commit is contained in:
AmirReza Jamali
2026-06-10 11:34:45 +03:30
parent aa9c9444fa
commit 633f0ba881
2 changed files with 125 additions and 47 deletions
+116
View File
@@ -0,0 +1,116 @@
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/logger.dart';
/// Interceptor responsible for everything authentication-related on the wire:
///
/// * injects the stored bearer token into every outgoing request, and
/// * reacts to `401 Unauthorized` responses by attempting a single token
/// refresh, replaying the original request, and—if that fails—logging the
/// user out and navigating them to the login screen via [onAuthFailed].
///
/// The refresh / login endpoints are skipped so a failed login or a dead
/// refresh token does not recurse back into the refresh flow.
class AuthInterceptor extends Interceptor {
AuthInterceptor({
required Dio dio,
required SharedPreferences prefs,
required Future<String?> Function() refreshToken,
required Future<void> Function()? Function() onAuthFailed,
}) : _dio = dio,
_prefs = prefs,
_refreshToken = refreshToken,
_onAuthFailed = onAuthFailed;
final Dio _dio;
final SharedPreferences _prefs;
/// Refreshes the access token, returning the new token or `null` on failure.
final Future<String?> Function() _refreshToken;
/// Resolves the (lazily-wired) logout/navigation callback. It is a getter so
/// that the callback can be registered after the interceptor is created,
/// without leaving the interceptor holding a stale `null` reference.
final Future<void> Function()? Function() _onAuthFailed;
/// Endpoints for which a 401 must NOT trigger a token refresh.
static const List<String> _authPaths = <String>[
'/api/v1/profile/login',
'/api/v1/profile/refresh-token',
];
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
appLogger.info('🌐 Request: ${options.method} ${options.path}');
final String? token = _prefs.getString('bearer');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
appLogger.info(
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
);
handler.next(response);
}
@override
Future<void> onError(
DioException error,
ErrorInterceptorHandler handler,
) async {
appLogger.error('❌ Error: ${error.message} - ${error.requestOptions.path}');
appLogger.error('❌ Error type: ${error.type}');
final bool isUnauthorized = error.response?.statusCode == 401;
final String path = error.requestOptions.path;
final bool isAuthEndpoint = _authPaths.any(path.contains);
// Only 401s on protected endpoints are interesting here.
if (!isUnauthorized || isAuthEndpoint) {
if (isAuthEndpoint) {
appLogger.info('🔄 Skipping token refresh for authentication endpoint');
}
return handler.next(error);
}
// Attempt a single refresh and replay the original request.
final String? newAccessToken = await _refreshToken();
if (newAccessToken != null) {
try {
final RequestOptions options = error.requestOptions
..headers['Authorization'] = 'Bearer $newAccessToken';
final Response<dynamic> response = await _dio.fetch(options);
return handler.resolve(response);
} on DioException catch (e) {
// Replay still failed (e.g. token revoked) fall through to logout.
appLogger.error('❌ Retry after token refresh failed: ${e.message}');
}
}
// Refresh (or the replay) failed → force logout and go to login.
appLogger.error('❌ Authentication failed, logging out');
await _logout();
return handler.next(error);
}
Future<void> _logout() async {
final Future<void> Function()? callback = _onAuthFailed();
if (callback != null) {
await callback();
return;
}
// Defensive fallback: if no logout/navigation callback has been wired yet,
// at least clear the stored credentials so the router's auth guard can
// redirect the user away from protected routes.
appLogger.error('⚠️ No auth-failed callback registered; clearing tokens');
await _prefs.remove('bearer');
await _prefs.remove('refresh_token');
await _prefs.remove('customer_data');
}
}
+9 -47
View File
@@ -9,6 +9,7 @@ import '../config/app_config.dart';
import '../utils/logger.dart';
import '../utils/result.dart';
import 'app_exceptions.dart';
import 'auth_interceptor.dart';
import 'network_connectivity.dart';
class NetworkManager {
@@ -45,54 +46,15 @@ class NetworkManager {
return;
}
final requestInterceptor = InterceptorsWrapper(
onRequest: (options, handler) {
appLogger.info('🌐 Request: ${options.method} ${options.path}');
String? token = _prefs.getString('bearer');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onResponse: (response, handler) {
appLogger.info(
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
);
return handler.next(response);
},
onError: (error, handler) async {
appLogger.error(
'❌ Error: ${error.message} - ${error.requestOptions.path}',
);
appLogger.error('❌ Error type: ${error.type}');
if (error.response?.statusCode == 401) {
// Skip refresh token for login and refresh token endpoints
final path = error.requestOptions.path;
if (path.contains('/api/v1/profile/login') ||
path.contains('/api/v1/profile/refresh-token')) {
appLogger.info(
'🔄 Skipping token refresh for authentication endpoints',
);
return handler.next(error);
}
final newAccessToken = await refreshToken();
if (newAccessToken != null) {
mainDio.options.headers['Authorization'] = 'Bearer $newAccessToken';
return handler.resolve(await mainDio.fetch(error.requestOptions));
}
// Refresh token failed force logout
appLogger.error('❌ Refresh token failed, triggering logout');
await _onAuthFailed!();
}
return handler.next(error);
},
mainDio.interceptors.add(
AuthInterceptor(
dio: mainDio,
prefs: _prefs,
refreshToken: refreshToken,
// Resolved lazily so the callback can be wired up after construction.
onAuthFailed: () => _onAuthFailed,
),
);
mainDio.interceptors.add(requestInterceptor);
_interceptorsAdded = true;
}