import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:injectable/injectable.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../app/app.dart'; import '../config/environment.dart'; import '../di/injection.dart'; import '../error/error_handler.dart'; import '../utils/logger.dart'; import '../../features/authentication/presentation/bloc/auth_bloc.dart'; import '../../features/authentication/data/datasources/keycloak_auth_service.dart'; import 'org_context_service.dart'; /// Client réseau unifié basé sur Dio (Version DRY & Minimaliste). @lazySingleton class ApiClient { late final Dio _dio; static const FlutterSecureStorage _storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device), ); ApiClient(OrgContextService orgContextService) { _dio = Dio( BaseOptions( baseUrl: AppConfig.apiBaseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 15), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), ); // Intercepteur de Log (Uniquement en Dev) if (AppConfig.enableLogging) { _dio.interceptors.add(LogInterceptor( requestHeader: true, requestBody: true, responseBody: true, logPrint: (obj) => print('🌐 [API] $obj'), )); } // Intercepteur de Token & Refresh automatique _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { // Utilise la clé 'kc_access' synchronisée avec KeycloakAuthService final token = await _storage.read(key: 'kc_access'); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } // Injecter l'organisation active si disponible if (orgContextService.hasContext) { options.headers[OrgContextService.headerName] = orgContextService.activeOrganisationId; } return handler.next(options); }, onError: (DioException e, handler) async { // Évite une boucle infinie si le retry échoue aussi avec 401 final isRetry = e.requestOptions.extra['custom_retry'] == true; if (e.response?.statusCode == 401 && !isRetry) { final responseBody = e.response?.data; debugPrint('🔑 [API] 401 Detected. Body: $responseBody. Attempting token refresh...'); final refreshed = await _refreshToken(); if (refreshed) { final token = await _storage.read(key: 'kc_access'); if (token != null) { // Marque la requête comme étant un retry final options = e.requestOptions; options.extra['custom_retry'] = true; options.headers['Authorization'] = 'Bearer $token'; try { debugPrint('🔄 [API] Retrying request: ${options.path}'); final response = await _dio.fetch(options); return handler.resolve(response); } on DioException catch (retryError) { final retryBody = retryError.response?.data; debugPrint('🚨 [API] Retry failed with status: ${retryError.response?.statusCode}. Body: $retryBody'); if (retryError.response?.statusCode == 401) { debugPrint('🚪 [API] Persistent 401. Force Logout.'); _forceLogout(); return handler.reject(DioException( requestOptions: retryError.requestOptions, type: DioExceptionType.cancel, )); } return handler.next(retryError); } catch (retryError) { debugPrint('🚨 [API] Retry critical error: $retryError'); return handler.next(e); } } } else { debugPrint('🚪 [API] Refresh failed. Force Logout.'); _forceLogout(); return handler.reject(DioException( requestOptions: e.requestOptions, type: DioExceptionType.cancel, )); } } return handler.next(e); }, ), ); } void _forceLogout() { try { UnionFlowApp.scaffoldMessengerKey.currentState ?..clearSnackBars() ..showSnackBar( SnackBar( content: const Row( children: [ Icon(Icons.lock_clock, color: Colors.white, size: 20), SizedBox(width: 10), Expanded( child: Text( 'Session expirée. Vous avez été déconnecté automatiquement.', style: TextStyle(color: Colors.white), ), ), ], ), backgroundColor: Colors.orange.shade700, duration: const Duration(seconds: 4), behavior: SnackBarBehavior.floating, ), ); final authBloc = getIt(); authBloc.add(const AuthLogoutRequested()); } catch (e, st) { AppLogger.error( 'ApiClient: force logout failed - ${ErrorHandler.getErrorMessage(e)}', error: e, stackTrace: st, ); } } Future _refreshToken() async { try { final authService = getIt(); final newToken = await authService.refreshToken(); return newToken != null; } catch (e, st) { AppLogger.error( 'ApiClient: refresh token failed - ${ErrorHandler.getErrorMessage(e)}', error: e, stackTrace: st, ); return false; } } Future> get(String path, {Map? queryParameters, Options? options}) => _dio.get(path, queryParameters: queryParameters, options: options); Future> post(String path, {dynamic data, Map? queryParameters, Options? options}) => _dio.post(path, data: data, queryParameters: queryParameters, options: options); Future> put(String path, {dynamic data, Map? queryParameters, Options? options}) => _dio.put(path, data: data, queryParameters: queryParameters, options: options); Future> delete(String path, {dynamic data, Map? queryParameters, Options? options}) => _dio.delete(path, data: data, queryParameters: queryParameters, options: options); }