first commit
This commit is contained in:
167
unionflow-mobile-apps/lib/core/network/auth_interceptor.dart
Normal file
167
unionflow-mobile-apps/lib/core/network/auth_interceptor.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../auth/storage/secure_token_storage.dart';
|
||||
|
||||
/// Interceptor pour gérer l'authentification automatique
|
||||
@singleton
|
||||
class AuthInterceptor extends Interceptor {
|
||||
final SecureTokenStorage _tokenStorage;
|
||||
|
||||
// Callback pour déclencher le refresh token
|
||||
void Function()? onTokenRefreshNeeded;
|
||||
|
||||
// Callback pour déconnecter l'utilisateur
|
||||
void Function()? onAuthenticationFailed;
|
||||
|
||||
AuthInterceptor(this._tokenStorage);
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
// Ignorer l'authentification pour certaines routes
|
||||
if (_shouldSkipAuth(options)) {
|
||||
handler.next(options);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Récupérer le token d'accès
|
||||
final accessToken = await _tokenStorage.getAccessToken();
|
||||
|
||||
if (accessToken != null) {
|
||||
// Vérifier si le token expire bientôt
|
||||
final isExpiringSoon = await _tokenStorage.isAccessTokenExpiringSoon();
|
||||
|
||||
if (isExpiringSoon) {
|
||||
// Déclencher le refresh token si nécessaire
|
||||
onTokenRefreshNeeded?.call();
|
||||
}
|
||||
|
||||
// Ajouter le token à l'en-tête Authorization
|
||||
options.headers['Authorization'] = 'Bearer $accessToken';
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
} catch (e) {
|
||||
// En cas d'erreur, continuer sans token
|
||||
print('Erreur lors de la récupération du token: $e');
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
// Traitement des réponses réussies
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// Gestion des erreurs d'authentification
|
||||
if (err.response?.statusCode == 401) {
|
||||
await _handle401Error(err, handler);
|
||||
} else if (err.response?.statusCode == 403) {
|
||||
await _handle403Error(err, handler);
|
||||
} else {
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les erreurs 401 (Non autorisé)
|
||||
Future<void> _handle401Error(DioException err, ErrorInterceptorHandler handler) async {
|
||||
try {
|
||||
// Vérifier si on a un refresh token valide
|
||||
final refreshToken = await _tokenStorage.getRefreshToken();
|
||||
final refreshExpiresAt = await _tokenStorage.getRefreshTokenExpirationDate();
|
||||
|
||||
if (refreshToken != null &&
|
||||
refreshExpiresAt != null &&
|
||||
DateTime.now().isBefore(refreshExpiresAt)) {
|
||||
|
||||
// Tentative de refresh du token
|
||||
onTokenRefreshNeeded?.call();
|
||||
|
||||
// Attendre un peu pour laisser le temps au refresh
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
// Retry de la requête originale avec le nouveau token
|
||||
final newAccessToken = await _tokenStorage.getAccessToken();
|
||||
if (newAccessToken != null) {
|
||||
final newRequest = await _retryRequest(err.requestOptions, newAccessToken);
|
||||
handler.resolve(newRequest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Si le refresh n'est pas possible ou a échoué, déconnecter l'utilisateur
|
||||
await _tokenStorage.clearAuthData();
|
||||
onAuthenticationFailed?.call();
|
||||
|
||||
} catch (e) {
|
||||
print('Erreur lors de la gestion de l\'erreur 401: $e');
|
||||
await _tokenStorage.clearAuthData();
|
||||
onAuthenticationFailed?.call();
|
||||
}
|
||||
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
/// Gère les erreurs 403 (Interdit)
|
||||
Future<void> _handle403Error(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// L'utilisateur n'a pas les permissions suffisantes
|
||||
// On peut logger cela ou rediriger vers une page d'erreur
|
||||
print('Accès interdit (403) pour: ${err.requestOptions.path}');
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
/// Retry une requête avec un nouveau token
|
||||
Future<Response> _retryRequest(RequestOptions options, String newAccessToken) async {
|
||||
final dio = Dio();
|
||||
|
||||
// Copier les options originales
|
||||
final newOptions = Options(
|
||||
method: options.method,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'Authorization': 'Bearer $newAccessToken',
|
||||
},
|
||||
extra: {'skipAuth': true}, // Éviter la récursion infinie
|
||||
);
|
||||
|
||||
// Effectuer la nouvelle requête
|
||||
return await dio.request(
|
||||
options.path,
|
||||
data: options.data,
|
||||
queryParameters: options.queryParameters,
|
||||
options: newOptions,
|
||||
);
|
||||
}
|
||||
|
||||
/// Détermine si l'authentification doit être ignorée pour une requête
|
||||
bool _shouldSkipAuth(RequestOptions options) {
|
||||
// Ignorer l'auth pour les routes publiques
|
||||
final publicPaths = [
|
||||
'/api/auth/login',
|
||||
'/api/auth/refresh',
|
||||
'/api/auth/info',
|
||||
'/api/auth/register',
|
||||
'/api/health',
|
||||
];
|
||||
|
||||
// Vérifier si le path est dans la liste des routes publiques
|
||||
final isPublicPath = publicPaths.any((path) => options.path.contains(path));
|
||||
|
||||
// Vérifier si l'option skipAuth est activée
|
||||
final skipAuth = options.extra['skipAuth'] == true;
|
||||
|
||||
return isPublicPath || skipAuth;
|
||||
}
|
||||
|
||||
/// Configuration des callbacks
|
||||
void setCallbacks({
|
||||
void Function()? onTokenRefreshNeeded,
|
||||
void Function()? onAuthenticationFailed,
|
||||
}) {
|
||||
this.onTokenRefreshNeeded = onTokenRefreshNeeded;
|
||||
this.onAuthenticationFailed = onAuthenticationFailed;
|
||||
}
|
||||
}
|
||||
113
unionflow-mobile-apps/lib/core/network/dio_client.dart
Normal file
113
unionflow-mobile-apps/lib/core/network/dio_client.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
import 'auth_interceptor.dart';
|
||||
|
||||
/// Configuration centralisée du client HTTP Dio
|
||||
@singleton
|
||||
class DioClient {
|
||||
late final Dio _dio;
|
||||
|
||||
DioClient() {
|
||||
_dio = Dio();
|
||||
_setupInterceptors();
|
||||
_configureOptions();
|
||||
}
|
||||
|
||||
Dio get dio => _dio;
|
||||
|
||||
void _configureOptions() {
|
||||
_dio.options = BaseOptions(
|
||||
// URL de base de l'API
|
||||
baseUrl: 'http://localhost:8081', // Adresse de votre API Quarkus
|
||||
|
||||
// Timeouts
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
sendTimeout: const Duration(seconds: 30),
|
||||
|
||||
// Headers par défaut
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'UnionFlow-Mobile/1.0.0',
|
||||
},
|
||||
|
||||
// Validation des codes de statut
|
||||
validateStatus: (status) {
|
||||
return status != null && status < 500;
|
||||
},
|
||||
|
||||
// Suivre les redirections
|
||||
followRedirects: true,
|
||||
maxRedirects: 3,
|
||||
|
||||
// Politique de persistance des cookies
|
||||
persistentConnection: true,
|
||||
|
||||
// Format de réponse par défaut
|
||||
responseType: ResponseType.json,
|
||||
);
|
||||
}
|
||||
|
||||
void _setupInterceptors() {
|
||||
// Interceptor de logging (seulement en debug)
|
||||
_dio.interceptors.add(
|
||||
PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
compact: true,
|
||||
maxWidth: 90,
|
||||
filter: (options, args) {
|
||||
// Ne pas logger les mots de passe
|
||||
if (options.path.contains('/auth/login')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Interceptor d'authentification (sera injecté plus tard)
|
||||
// Il sera ajouté dans AuthService pour éviter les dépendances circulaires
|
||||
}
|
||||
|
||||
/// Ajoute l'interceptor d'authentification
|
||||
void addAuthInterceptor(AuthInterceptor authInterceptor) {
|
||||
_dio.interceptors.add(authInterceptor);
|
||||
}
|
||||
|
||||
/// Configure l'URL de base
|
||||
void setBaseUrl(String baseUrl) {
|
||||
_dio.options.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/// Ajoute un header global
|
||||
void addHeader(String key, String value) {
|
||||
_dio.options.headers[key] = value;
|
||||
}
|
||||
|
||||
/// Supprime un header global
|
||||
void removeHeader(String key) {
|
||||
_dio.options.headers.remove(key);
|
||||
}
|
||||
|
||||
/// Configure les timeouts
|
||||
void setTimeout({
|
||||
Duration? connect,
|
||||
Duration? receive,
|
||||
Duration? send,
|
||||
}) {
|
||||
if (connect != null) _dio.options.connectTimeout = connect;
|
||||
if (receive != null) _dio.options.receiveTimeout = receive;
|
||||
if (send != null) _dio.options.sendTimeout = send;
|
||||
}
|
||||
|
||||
/// Nettoie et ferme le client
|
||||
void dispose() {
|
||||
_dio.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user