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:
@@ -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,6 +9,7 @@ import '../config/app_config.dart';
|
|||||||
import '../utils/logger.dart';
|
import '../utils/logger.dart';
|
||||||
import '../utils/result.dart';
|
import '../utils/result.dart';
|
||||||
import 'app_exceptions.dart';
|
import 'app_exceptions.dart';
|
||||||
|
import 'auth_interceptor.dart';
|
||||||
import 'network_connectivity.dart';
|
import 'network_connectivity.dart';
|
||||||
|
|
||||||
class NetworkManager {
|
class NetworkManager {
|
||||||
@@ -45,54 +46,15 @@ class NetworkManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final requestInterceptor = InterceptorsWrapper(
|
mainDio.interceptors.add(
|
||||||
onRequest: (options, handler) {
|
AuthInterceptor(
|
||||||
appLogger.info('🌐 Request: ${options.method} ${options.path}');
|
dio: mainDio,
|
||||||
String? token = _prefs.getString('bearer');
|
prefs: _prefs,
|
||||||
if (token != null) {
|
refreshToken: refreshToken,
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
// Resolved lazily so the callback can be wired up after construction.
|
||||||
}
|
onAuthFailed: () => _onAuthFailed,
|
||||||
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(requestInterceptor);
|
|
||||||
_interceptorsAdded = true;
|
_interceptorsAdded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user