first commit

This commit is contained in:
DahoudG
2025-08-20 21:00:35 +00:00
commit b2a23bdf89
583 changed files with 243074 additions and 0 deletions

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

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