From 633f0ba88102d72f3c35120290bff7ebeaab865a Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Wed, 10 Jun 2026 11:34:45 +0330 Subject: [PATCH] Refactor network request handling by introducing AuthInterceptor for improved authentication management. Removed the previous request interceptor logic to streamline the code and enhance maintainability. --- lib/core/network/auth_interceptor.dart | 116 +++++++++++++++++++++++++ lib/core/network/network_manager.dart | 56 ++---------- 2 files changed, 125 insertions(+), 47 deletions(-) create mode 100644 lib/core/network/auth_interceptor.dart diff --git a/lib/core/network/auth_interceptor.dart b/lib/core/network/auth_interceptor.dart new file mode 100644 index 0000000..2326dc2 --- /dev/null +++ b/lib/core/network/auth_interceptor.dart @@ -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 Function() refreshToken, + required Future 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 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 Function()? Function() _onAuthFailed; + + /// Endpoints for which a 401 must NOT trigger a token refresh. + static const List _authPaths = [ + '/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 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 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 _logout() async { + final Future 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'); + } +} diff --git a/lib/core/network/network_manager.dart b/lib/core/network/network_manager.dart index 4ebce31..cab3333 100644 --- a/lib/core/network/network_manager.dart +++ b/lib/core/network/network_manager.dart @@ -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; }