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,203 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../models/auth_state.dart';
import '../services/auth_service.dart';
import '../services/auth_api_service.dart';
import 'auth_event.dart';
/// BLoC pour gérer l'authentification
@singleton
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthService _authService;
late StreamSubscription<AuthState> _authStateSubscription;
AuthBloc(this._authService) : super(const AuthState.unknown()) {
// Écouter les changements d'état du service
_authStateSubscription = _authService.authStateStream.listen(
(authState) => add(AuthStateChanged(authState)),
);
// Gestionnaires d'événements
on<AuthInitializeRequested>(_onInitializeRequested);
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
on<AuthTokenRefreshRequested>(_onTokenRefreshRequested);
on<AuthSessionExpired>(_onSessionExpired);
on<AuthStatusCheckRequested>(_onStatusCheckRequested);
on<AuthErrorCleared>(_onErrorCleared);
on<AuthStateChanged>(_onStateChanged);
}
/// Initialisation de l'authentification
Future<void> _onInitializeRequested(
AuthInitializeRequested event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.checking());
try {
await _authService.initialize();
} catch (e) {
emit(AuthState.error('Erreur d\'initialisation: $e'));
}
}
/// Gestion de la connexion
Future<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
try {
await _authService.login(event.loginRequest);
// L'état sera mis à jour par le stream du service
} on AuthApiException catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: e.message,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: 'Erreur de connexion: $e',
));
}
}
/// Gestion de la déconnexion
Future<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
await _authService.logout();
// L'état sera mis à jour par le stream du service
} catch (e) {
// Même en cas d'erreur, on considère que la déconnexion locale a réussi
emit(const AuthState.unauthenticated());
}
}
/// Gestion du rafraîchissement de token
Future<void> _onTokenRefreshRequested(
AuthTokenRefreshRequested event,
Emitter<AuthState> emit,
) async {
// Le rafraîchissement est géré automatiquement par le service
// Cet événement peut être utilisé pour forcer un rafraîchissement manuel
try {
// Le service gère déjà le rafraîchissement automatique
// On peut ajouter ici une logique spécifique si nécessaire
} catch (e) {
emit(AuthState.error('Erreur lors du rafraîchissement: $e'));
}
}
/// Gestion de l'expiration de session
Future<void> _onSessionExpired(
AuthSessionExpired event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.expired());
// Optionnel: essayer un rafraîchissement automatique
try {
await _authService.logout();
} catch (e) {
// Ignorer les erreurs de déconnexion lors de l'expiration
}
}
/// Vérification du statut d'authentification
Future<void> _onStatusCheckRequested(
AuthStatusCheckRequested event,
Emitter<AuthState> emit,
) async {
// Utiliser l'état actuel du service
final currentServiceState = _authService.currentState;
if (currentServiceState != state) {
emit(currentServiceState);
}
}
/// Nettoyage des erreurs
void _onErrorCleared(
AuthErrorCleared event,
Emitter<AuthState> emit,
) {
if (state.errorMessage != null) {
emit(state.copyWith(errorMessage: null));
}
}
/// Mise à jour depuis le service d'authentification
void _onStateChanged(
AuthStateChanged event,
Emitter<AuthState> emit,
) {
final newState = event.authState as AuthState;
// Émettre le nouvel état seulement s'il a changé
if (newState != state) {
emit(newState);
}
}
/// Vérifie si l'utilisateur est connecté
bool get isAuthenticated => state.isAuthenticated;
/// Récupère l'utilisateur actuel
get currentUser => state.user;
/// Vérifie si l'utilisateur a un rôle spécifique
bool hasRole(String role) {
return _authService.hasRole(role);
}
/// Vérifie si l'utilisateur a un des rôles spécifiés
bool hasAnyRole(List<String> roles) {
return _authService.hasAnyRole(roles);
}
/// Vérifie si la session expire bientôt
bool get isSessionExpiringSoon => state.isExpiringSoon;
/// Récupère le message d'erreur formaté
String? get errorMessage {
final error = state.errorMessage;
if (error == null) return null;
// Formatage des messages d'erreur pour l'utilisateur
if (error.contains('network') || error.contains('connexion')) {
return 'Problème de connexion. Vérifiez votre réseau.';
}
if (error.contains('401') || error.contains('Identifiants')) {
return 'Email ou mot de passe incorrect.';
}
if (error.contains('403')) {
return 'Accès non autorisé.';
}
if (error.contains('timeout')) {
return 'Délai d\'attente dépassé. Réessayez.';
}
if (error.contains('server') || error.contains('500')) {
return 'Erreur serveur temporaire. Réessayez plus tard.';
}
return error;
}
@override
Future<void> close() {
_authStateSubscription.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,60 @@
import 'package:equatable/equatable.dart';
import '../models/login_request.dart';
/// Événements d'authentification
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
/// Initialiser l'authentification
class AuthInitializeRequested extends AuthEvent {
const AuthInitializeRequested();
}
/// Demande de connexion
class AuthLoginRequested extends AuthEvent {
final LoginRequest loginRequest;
const AuthLoginRequested(this.loginRequest);
@override
List<Object> get props => [loginRequest];
}
/// Demande de déconnexion
class AuthLogoutRequested extends AuthEvent {
const AuthLogoutRequested();
}
/// Demande de rafraîchissement de token
class AuthTokenRefreshRequested extends AuthEvent {
const AuthTokenRefreshRequested();
}
/// Session expirée
class AuthSessionExpired extends AuthEvent {
const AuthSessionExpired();
}
/// Vérification de l'état d'authentification
class AuthStatusCheckRequested extends AuthEvent {
const AuthStatusCheckRequested();
}
/// Réinitialisation de l'erreur
class AuthErrorCleared extends AuthEvent {
const AuthErrorCleared();
}
/// Changement d'état depuis le service
class AuthStateChanged extends AuthEvent {
final dynamic authState;
const AuthStateChanged(this.authState);
@override
List<Object?> get props => [authState];
}

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/auth_state.dart';
import '../services/temp_auth_service.dart';
import 'auth_event.dart';
/// BLoC temporaire pour test sans injection de dépendances
class TempAuthBloc extends Bloc<AuthEvent, AuthState> {
final TempAuthService _authService;
late StreamSubscription<AuthState> _authStateSubscription;
TempAuthBloc(this._authService) : super(const AuthState.unknown()) {
_authStateSubscription = _authService.authStateStream.listen(
(authState) => add(AuthStateChanged(authState)),
);
on<AuthInitializeRequested>(_onInitializeRequested);
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
on<AuthErrorCleared>(_onErrorCleared);
on<AuthStateChanged>(_onStateChanged);
}
Future<void> _onInitializeRequested(
AuthInitializeRequested event,
Emitter<AuthState> emit,
) async {
await _authService.initialize();
}
Future<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
try {
await _authService.login(event.loginRequest);
} catch (e) {
emit(AuthState.error(e.toString()));
}
}
Future<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
await _authService.logout();
}
void _onErrorCleared(
AuthErrorCleared event,
Emitter<AuthState> emit,
) {
if (state.errorMessage != null) {
emit(state.copyWith(errorMessage: null));
}
}
void _onStateChanged(
AuthStateChanged event,
Emitter<AuthState> emit,
) {
final newState = event.authState as AuthState;
if (newState != state) {
emit(newState);
}
}
@override
Future<void> close() {
_authStateSubscription.cancel();
_authService.dispose();
return super.close();
}
}

View File

@@ -0,0 +1,140 @@
import 'package:equatable/equatable.dart';
import 'user_info.dart';
/// États d'authentification possibles
enum AuthStatus {
unknown, // État initial
checking, // Vérification en cours
authenticated,// Utilisateur connecté
unauthenticated, // Utilisateur non connecté
expired, // Session expirée
error, // Erreur d'authentification
}
/// État d'authentification de l'application
class AuthState extends Equatable {
final AuthStatus status;
final UserInfo? user;
final String? accessToken;
final String? refreshToken;
final DateTime? expiresAt;
final String? errorMessage;
final bool isLoading;
const AuthState({
this.status = AuthStatus.unknown,
this.user,
this.accessToken,
this.refreshToken,
this.expiresAt,
this.errorMessage,
this.isLoading = false,
});
/// État initial inconnu
const AuthState.unknown() : this(status: AuthStatus.unknown);
/// État de vérification
const AuthState.checking() : this(
status: AuthStatus.checking,
isLoading: true,
);
/// État authentifié
const AuthState.authenticated({
required UserInfo user,
required String accessToken,
required String refreshToken,
required DateTime expiresAt,
}) : this(
status: AuthStatus.authenticated,
user: user,
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: expiresAt,
isLoading: false,
);
/// État non authentifié
const AuthState.unauthenticated({String? errorMessage}) : this(
status: AuthStatus.unauthenticated,
errorMessage: errorMessage,
isLoading: false,
);
/// État de session expirée
const AuthState.expired() : this(
status: AuthStatus.expired,
isLoading: false,
);
/// État d'erreur
const AuthState.error(String errorMessage) : this(
status: AuthStatus.error,
errorMessage: errorMessage,
isLoading: false,
);
/// Vérifie si l'utilisateur est connecté
bool get isAuthenticated => status == AuthStatus.authenticated;
/// Vérifie si la session est valide
bool get isSessionValid {
if (!isAuthenticated || expiresAt == null) return false;
return DateTime.now().isBefore(expiresAt!);
}
/// Vérifie si la session expire bientôt
bool get isExpiringSoon {
if (!isAuthenticated || expiresAt == null) return false;
final threshold = DateTime.now().add(const Duration(minutes: 5));
return expiresAt!.isBefore(threshold);
}
/// Crée une copie avec des modifications
AuthState copyWith({
AuthStatus? status,
UserInfo? user,
String? accessToken,
String? refreshToken,
DateTime? expiresAt,
String? errorMessage,
bool? isLoading,
}) {
return AuthState(
status: status ?? this.status,
user: user ?? this.user,
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
expiresAt: expiresAt ?? this.expiresAt,
errorMessage: errorMessage ?? this.errorMessage,
isLoading: isLoading ?? this.isLoading,
);
}
/// Crée une copie en effaçant les données sensibles
AuthState clearSensitiveData() {
return AuthState(
status: status,
user: user,
errorMessage: errorMessage,
isLoading: isLoading,
);
}
@override
List<Object?> get props => [
status,
user,
accessToken,
refreshToken,
expiresAt,
errorMessage,
isLoading,
];
@override
String toString() {
return 'AuthState(status: $status, user: ${user?.email}, isLoading: $isLoading, error: $errorMessage)';
}
}

View File

@@ -0,0 +1,50 @@
import 'package:equatable/equatable.dart';
/// Modèle de requête de connexion
class LoginRequest extends Equatable {
final String email;
final String password;
final bool rememberMe;
const LoginRequest({
required this.email,
required this.password,
this.rememberMe = false,
});
Map<String, dynamic> toJson() {
return {
'email': email,
'password': password,
'rememberMe': rememberMe,
};
}
factory LoginRequest.fromJson(Map<String, dynamic> json) {
return LoginRequest(
email: json['email'] ?? '',
password: json['password'] ?? '',
rememberMe: json['rememberMe'] ?? false,
);
}
LoginRequest copyWith({
String? email,
String? password,
bool? rememberMe,
}) {
return LoginRequest(
email: email ?? this.email,
password: password ?? this.password,
rememberMe: rememberMe ?? this.rememberMe,
);
}
@override
List<Object?> get props => [email, password, rememberMe];
@override
String toString() {
return 'LoginRequest(email: $email, rememberMe: $rememberMe)';
}
}

View File

@@ -0,0 +1,96 @@
import 'package:equatable/equatable.dart';
import 'user_info.dart';
/// Modèle de réponse de connexion
class LoginResponse extends Equatable {
final String accessToken;
final String refreshToken;
final String tokenType;
final DateTime expiresAt;
final DateTime refreshExpiresAt;
final UserInfo user;
const LoginResponse({
required this.accessToken,
required this.refreshToken,
required this.tokenType,
required this.expiresAt,
required this.refreshExpiresAt,
required this.user,
});
/// Vérifie si le token d'accès est expiré
bool get isAccessTokenExpired {
return DateTime.now().isAfter(expiresAt);
}
/// Vérifie si le refresh token est expiré
bool get isRefreshTokenExpired {
return DateTime.now().isAfter(refreshExpiresAt);
}
/// Vérifie si le token expire dans les prochaines minutes
bool isExpiringSoon({int minutes = 5}) {
final threshold = DateTime.now().add(Duration(minutes: minutes));
return expiresAt.isBefore(threshold);
}
factory LoginResponse.fromJson(Map<String, dynamic> json) {
return LoginResponse(
accessToken: json['accessToken'] ?? '',
refreshToken: json['refreshToken'] ?? '',
tokenType: json['tokenType'] ?? 'Bearer',
expiresAt: json['expiresAt'] != null
? DateTime.parse(json['expiresAt'])
: DateTime.now().add(const Duration(minutes: 15)),
refreshExpiresAt: json['refreshExpiresAt'] != null
? DateTime.parse(json['refreshExpiresAt'])
: DateTime.now().add(const Duration(days: 7)),
user: UserInfo.fromJson(json['user'] ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'accessToken': accessToken,
'refreshToken': refreshToken,
'tokenType': tokenType,
'expiresAt': expiresAt.toIso8601String(),
'refreshExpiresAt': refreshExpiresAt.toIso8601String(),
'user': user.toJson(),
};
}
LoginResponse copyWith({
String? accessToken,
String? refreshToken,
String? tokenType,
DateTime? expiresAt,
DateTime? refreshExpiresAt,
UserInfo? user,
}) {
return LoginResponse(
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
tokenType: tokenType ?? this.tokenType,
expiresAt: expiresAt ?? this.expiresAt,
refreshExpiresAt: refreshExpiresAt ?? this.refreshExpiresAt,
user: user ?? this.user,
);
}
@override
List<Object?> get props => [
accessToken,
refreshToken,
tokenType,
expiresAt,
refreshExpiresAt,
user,
];
@override
String toString() {
return 'LoginResponse(tokenType: $tokenType, user: ${user.email}, expiresAt: $expiresAt)';
}
}

View File

@@ -0,0 +1,5 @@
// Export all auth models
export 'auth_state.dart';
export 'login_request.dart';
export 'login_response.dart';
export 'user_info.dart';

View File

@@ -0,0 +1,90 @@
import 'package:equatable/equatable.dart';
/// Modèle des informations utilisateur
class UserInfo extends Equatable {
final String id;
final String email;
final String firstName;
final String lastName;
final String role;
final String? profilePicture;
final bool isActive;
const UserInfo({
required this.id,
required this.email,
required this.firstName,
required this.lastName,
required this.role,
this.profilePicture,
required this.isActive,
});
String get fullName => '$firstName $lastName';
String get initials {
final f = firstName.isNotEmpty ? firstName[0] : '';
final l = lastName.isNotEmpty ? lastName[0] : '';
return '$f$l'.toUpperCase();
}
factory UserInfo.fromJson(Map<String, dynamic> json) {
return UserInfo(
id: json['id'] ?? '',
email: json['email'] ?? '',
firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '',
role: json['role'] ?? 'membre',
profilePicture: json['profilePicture'],
isActive: json['isActive'] ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'firstName': firstName,
'lastName': lastName,
'role': role,
'profilePicture': profilePicture,
'isActive': isActive,
};
}
UserInfo copyWith({
String? id,
String? email,
String? firstName,
String? lastName,
String? role,
String? profilePicture,
bool? isActive,
}) {
return UserInfo(
id: id ?? this.id,
email: email ?? this.email,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
role: role ?? this.role,
profilePicture: profilePicture ?? this.profilePicture,
isActive: isActive ?? this.isActive,
);
}
@override
List<Object?> get props => [
id,
email,
firstName,
lastName,
role,
profilePicture,
isActive,
];
@override
String toString() {
return 'UserInfo(id: $id, email: $email, fullName: $fullName, role: $role, isActive: $isActive)';
}
}

View File

@@ -0,0 +1,306 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import '../../../core/network/dio_client.dart';
import '../models/login_request.dart';
import '../models/login_response.dart';
/// Service API pour l'authentification
@singleton
class AuthApiService {
final DioClient _dioClient;
late final Dio _dio;
AuthApiService(this._dioClient) {
_dio = _dioClient.dio;
}
/// Effectue la connexion utilisateur
Future<LoginResponse> login(LoginRequest request) async {
try {
final response = await _dio.post(
'/api/auth/login',
data: request.toJson(),
options: Options(
headers: {
'Content-Type': 'application/json',
},
// Désactiver l'interceptor d'auth pour cette requête
extra: {'skipAuth': true},
),
);
if (response.statusCode == 200) {
return LoginResponse.fromJson(response.data);
} else {
throw AuthApiException(
'Erreur de connexion',
statusCode: response.statusCode,
response: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur inattendue lors de la connexion: $e');
}
}
/// Rafraîchit le token d'accès
Future<LoginResponse> refreshToken(String refreshToken) async {
try {
final response = await _dio.post(
'/api/auth/refresh',
data: {'refreshToken': refreshToken},
options: Options(
headers: {
'Content-Type': 'application/json',
},
extra: {'skipAuth': true},
),
);
if (response.statusCode == 200) {
return LoginResponse.fromJson(response.data);
} else {
throw AuthApiException(
'Erreur lors du rafraîchissement du token',
statusCode: response.statusCode,
response: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur inattendue lors du rafraîchissement: $e');
}
}
/// Effectue la déconnexion
Future<void> logout(String? refreshToken) async {
try {
await _dio.post(
'/api/auth/logout',
data: refreshToken != null ? {'refreshToken': refreshToken} : {},
options: Options(
headers: {
'Content-Type': 'application/json',
},
extra: {'skipAuth': true},
),
);
} on DioException catch (e) {
// Ignorer les erreurs de déconnexion côté serveur
// La déconnexion locale est plus importante
print('Erreur lors de la déconnexion serveur: ${e.message}');
} catch (e) {
print('Erreur inattendue lors de la déconnexion: $e');
}
}
/// Valide un token côté serveur
Future<bool> validateToken(String accessToken) async {
try {
final response = await _dio.get(
'/api/auth/validate',
options: Options(
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
},
extra: {'skipAuth': true},
),
);
return response.statusCode == 200;
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
return false;
}
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur lors de la validation du token: $e');
}
}
/// Récupère les informations de l'API d'authentification
Future<Map<String, dynamic>> getAuthInfo() async {
try {
final response = await _dio.get(
'/api/auth/info',
options: Options(
extra: {'skipAuth': true},
),
);
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>;
} else {
throw AuthApiException(
'Erreur lors de la récupération des informations',
statusCode: response.statusCode,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur inattendue: $e');
}
}
/// Gestion centralisée des erreurs Dio
AuthApiException _handleDioException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return AuthApiException(
'Délai d\'attente dépassé. Vérifiez votre connexion internet.',
type: AuthErrorType.timeout,
);
case DioExceptionType.connectionError:
return AuthApiException(
'Impossible de se connecter au serveur. Vérifiez votre connexion internet.',
type: AuthErrorType.network,
);
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
final data = e.response?.data;
switch (statusCode) {
case 400:
return AuthApiException(
_extractErrorMessage(data) ?? 'Données de requête invalides',
statusCode: statusCode,
type: AuthErrorType.validation,
response: data,
);
case 401:
return AuthApiException(
_extractErrorMessage(data) ?? 'Identifiants invalides',
statusCode: statusCode,
type: AuthErrorType.unauthorized,
response: data,
);
case 403:
return AuthApiException(
_extractErrorMessage(data) ?? 'Accès interdit',
statusCode: statusCode,
type: AuthErrorType.forbidden,
response: data,
);
case 429:
return AuthApiException(
'Trop de tentatives. Veuillez réessayer plus tard.',
statusCode: statusCode,
type: AuthErrorType.rateLimited,
response: data,
);
case 500:
case 502:
case 503:
case 504:
return AuthApiException(
'Erreur serveur temporaire. Veuillez réessayer.',
statusCode: statusCode,
type: AuthErrorType.server,
response: data,
);
default:
return AuthApiException(
_extractErrorMessage(data) ?? 'Erreur serveur inconnue',
statusCode: statusCode,
response: data,
);
}
case DioExceptionType.cancel:
return AuthApiException(
'Requête annulée',
type: AuthErrorType.cancelled,
);
default:
return AuthApiException(
'Erreur réseau: ${e.message}',
type: AuthErrorType.unknown,
);
}
}
/// Extrait le message d'erreur de la réponse
String? _extractErrorMessage(dynamic data) {
if (data == null) return null;
if (data is Map<String, dynamic>) {
return data['message'] ?? data['error'] ?? data['detail'];
}
if (data is String) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
return json['message'] ?? json['error'] ?? json['detail'];
} catch (_) {
return data;
}
}
return null;
}
}
/// Types d'erreurs d'authentification
enum AuthErrorType {
validation,
unauthorized,
forbidden,
timeout,
network,
server,
rateLimited,
cancelled,
unknown,
}
/// Exception spécifique à l'API d'authentification
class AuthApiException implements Exception {
final String message;
final int? statusCode;
final AuthErrorType type;
final dynamic response;
const AuthApiException(
this.message, {
this.statusCode,
this.type = AuthErrorType.unknown,
this.response,
});
bool get isNetworkError =>
type == AuthErrorType.network ||
type == AuthErrorType.timeout;
bool get isServerError => type == AuthErrorType.server;
bool get isClientError =>
type == AuthErrorType.validation ||
type == AuthErrorType.unauthorized ||
type == AuthErrorType.forbidden;
bool get isRetryable =>
isNetworkError ||
isServerError ||
type == AuthErrorType.rateLimited;
@override
String toString() {
return 'AuthApiException: $message ${statusCode != null ? '(Status: $statusCode)' : ''}';
}
}

View File

@@ -0,0 +1,318 @@
import 'dart:async';
import 'package:injectable/injectable.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import '../models/auth_state.dart';
import '../models/login_request.dart';
import '../models/login_response.dart';
import '../models/user_info.dart';
import '../storage/secure_token_storage.dart';
import 'auth_api_service.dart';
import '../../network/auth_interceptor.dart';
import '../../network/dio_client.dart';
/// Service principal d'authentification
@singleton
class AuthService {
final SecureTokenStorage _tokenStorage;
final AuthApiService _apiService;
final AuthInterceptor _authInterceptor;
final DioClient _dioClient;
// Stream controllers pour notifier les changements d'état
final _authStateController = StreamController<AuthState>.broadcast();
final _tokenRefreshController = StreamController<void>.broadcast();
// Timers pour la gestion automatique des tokens
Timer? _tokenRefreshTimer;
Timer? _sessionExpiryTimer;
// État actuel
AuthState _currentState = const AuthState.unknown();
AuthService(
this._tokenStorage,
this._apiService,
this._authInterceptor,
this._dioClient,
) {
_initializeAuthInterceptor();
}
// Getters
Stream<AuthState> get authStateStream => _authStateController.stream;
AuthState get currentState => _currentState;
bool get isAuthenticated => _currentState.isAuthenticated;
UserInfo? get currentUser => _currentState.user;
/// Initialise l'interceptor d'authentification
void _initializeAuthInterceptor() {
_authInterceptor.setCallbacks(
onTokenRefreshNeeded: () => _refreshTokenSilently(),
onAuthenticationFailed: () => logout(),
);
_dioClient.addAuthInterceptor(_authInterceptor);
}
/// Initialise le service d'authentification
Future<void> initialize() async {
_updateState(const AuthState.checking());
try {
// Vérifier si des tokens existent
final hasTokens = await _tokenStorage.hasAuthData();
if (!hasTokens) {
_updateState(const AuthState.unauthenticated());
return;
}
// Récupérer les données d'authentification
final authData = await _tokenStorage.getAuthData();
if (authData == null) {
_updateState(const AuthState.unauthenticated());
return;
}
// Vérifier si les tokens sont expirés
if (authData.isRefreshTokenExpired) {
await _tokenStorage.clearAuthData();
_updateState(const AuthState.unauthenticated());
return;
}
// Si le token d'accès est expiré, essayer de le rafraîchir
if (authData.isAccessTokenExpired) {
await _refreshToken();
return;
}
// Valider le token côté serveur
final isValid = await _validateTokenWithServer(authData.accessToken);
if (!isValid) {
await _refreshToken();
return;
}
// Tout est OK, restaurer la session
_updateState(AuthState.authenticated(
user: authData.user,
accessToken: authData.accessToken,
refreshToken: authData.refreshToken,
expiresAt: authData.expiresAt,
));
_scheduleTokenRefresh();
_scheduleSessionExpiry();
} catch (e) {
print('Erreur lors de l\'initialisation de l\'auth: $e');
await _tokenStorage.clearAuthData();
_updateState(AuthState.error('Erreur d\'initialisation: $e'));
}
}
/// Connecte un utilisateur
Future<void> login(LoginRequest request) async {
_updateState(_currentState.copyWith(isLoading: true));
try {
// Appel API de connexion
final response = await _apiService.login(request);
// Sauvegarder les tokens
await _tokenStorage.saveAuthData(response);
// Mettre à jour l'état
_updateState(AuthState.authenticated(
user: response.user,
accessToken: response.accessToken,
refreshToken: response.refreshToken,
expiresAt: response.expiresAt,
));
// Programmer les rafraîchissements
_scheduleTokenRefresh();
_scheduleSessionExpiry();
} on AuthApiException catch (e) {
_updateState(AuthState.error(e.message));
rethrow;
} catch (e) {
final errorMessage = 'Erreur de connexion: $e';
_updateState(AuthState.error(errorMessage));
rethrow;
}
}
/// Déconnecte l'utilisateur
Future<void> logout() async {
try {
// Récupérer le refresh token pour l'invalider côté serveur
final refreshToken = await _tokenStorage.getRefreshToken();
// Appel API de déconnexion (optionnel)
await _apiService.logout(refreshToken);
} catch (e) {
print('Erreur lors de la déconnexion serveur: $e');
}
// Nettoyage local (toujours fait)
await _tokenStorage.clearAuthData();
_cancelTimers();
_updateState(const AuthState.unauthenticated());
}
/// Rafraîchit le token d'accès
Future<void> _refreshToken() async {
try {
final refreshToken = await _tokenStorage.getRefreshToken();
if (refreshToken == null) {
throw Exception('Aucun refresh token disponible');
}
// Vérifier si le refresh token est expiré
final refreshExpiresAt = await _tokenStorage.getRefreshTokenExpirationDate();
if (refreshExpiresAt != null && DateTime.now().isAfter(refreshExpiresAt)) {
throw Exception('Refresh token expiré');
}
// Appel API de refresh
final response = await _apiService.refreshToken(refreshToken);
// Mettre à jour le stockage
await _tokenStorage.updateAccessToken(response.accessToken, response.expiresAt);
// Mettre à jour l'état
if (_currentState.isAuthenticated) {
_updateState(_currentState.copyWith(
accessToken: response.accessToken,
expiresAt: response.expiresAt,
));
} else {
_updateState(AuthState.authenticated(
user: response.user,
accessToken: response.accessToken,
refreshToken: response.refreshToken,
expiresAt: response.expiresAt,
));
}
// Reprogrammer les timers
_scheduleTokenRefresh();
} catch (e) {
print('Erreur lors du rafraîchissement du token: $e');
await logout();
}
}
/// Rafraîchit le token silencieusement (sans changer l'état de loading)
Future<void> _refreshTokenSilently() async {
try {
await _refreshToken();
_tokenRefreshController.add(null);
} catch (e) {
print('Erreur lors du rafraîchissement silencieux: $e');
}
}
/// Valide un token côté serveur
Future<bool> _validateTokenWithServer(String accessToken) async {
try {
return await _apiService.validateToken(accessToken);
} catch (e) {
print('Erreur lors de la validation du token: $e');
return false;
}
}
/// Programme le rafraîchissement automatique du token
void _scheduleTokenRefresh() {
_tokenRefreshTimer?.cancel();
if (!_currentState.isAuthenticated || _currentState.expiresAt == null) {
return;
}
// Rafraîchir 5 minutes avant l'expiration
final refreshTime = _currentState.expiresAt!.subtract(const Duration(minutes: 5));
final delay = refreshTime.difference(DateTime.now());
if (delay.isNegative) {
// Le token expire bientôt, rafraîchir immédiatement
_refreshTokenSilently();
return;
}
_tokenRefreshTimer = Timer(delay, () => _refreshTokenSilently());
}
/// Programme l'expiration de la session
void _scheduleSessionExpiry() {
_sessionExpiryTimer?.cancel();
if (!_currentState.isAuthenticated || _currentState.expiresAt == null) {
return;
}
final delay = _currentState.expiresAt!.difference(DateTime.now());
if (delay.isNegative) {
logout();
return;
}
_sessionExpiryTimer = Timer(delay, () {
_updateState(const AuthState.expired());
});
}
/// Annule tous les timers
void _cancelTimers() {
_tokenRefreshTimer?.cancel();
_sessionExpiryTimer?.cancel();
_tokenRefreshTimer = null;
_sessionExpiryTimer = null;
}
/// Met à jour l'état et notifie les listeners
void _updateState(AuthState newState) {
_currentState = newState;
_authStateController.add(newState);
}
/// Nettoie les ressources
void dispose() {
_cancelTimers();
_authStateController.close();
_tokenRefreshController.close();
}
/// Vérifie les permissions de l'utilisateur
bool hasRole(String role) {
return _currentState.user?.role == role;
}
/// Vérifie si l'utilisateur a un des rôles spécifiés
bool hasAnyRole(List<String> roles) {
final userRole = _currentState.user?.role;
return userRole != null && roles.contains(userRole);
}
/// Décode un token JWT (utilitaire)
Map<String, dynamic>? decodeToken(String token) {
try {
return JwtDecoder.decode(token);
} catch (e) {
print('Erreur lors du décodage du token: $e');
return null;
}
}
/// Vérifie si un token est expiré
bool isTokenExpired(String token) {
try {
return JwtDecoder.isExpired(token);
} catch (e) {
return true;
}
}
}

View File

@@ -0,0 +1,70 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/auth_state.dart';
import '../models/login_request.dart';
import '../models/user_info.dart';
/// Service d'authentification temporaire pour test sans dépendances
class TempAuthService {
final _authStateController = StreamController<AuthState>.broadcast();
AuthState _currentState = const AuthState.unknown();
Stream<AuthState> get authStateStream => _authStateController.stream;
AuthState get currentState => _currentState;
bool get isAuthenticated => _currentState.isAuthenticated;
UserInfo? get currentUser => _currentState.user;
Future<void> initialize() async {
_updateState(const AuthState.checking());
// Simuler une vérification
await Future.delayed(const Duration(seconds: 2));
_updateState(const AuthState.unauthenticated());
}
Future<void> login(LoginRequest request) async {
_updateState(_currentState.copyWith(isLoading: true));
try {
// Simulation d'appel API
await Future.delayed(const Duration(seconds: 1));
// Vérification simple pour la démo
if (request.email == 'admin@unionflow.dev' && request.password == 'admin123') {
final user = UserInfo(
id: '1',
email: request.email,
firstName: 'Admin',
lastName: 'UnionFlow',
role: 'admin',
isActive: true,
);
_updateState(AuthState.authenticated(
user: user,
accessToken: 'fake_access_token',
refreshToken: 'fake_refresh_token',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
));
} else {
throw Exception('Identifiants invalides');
}
} catch (e) {
_updateState(AuthState.error(e.toString()));
}
}
Future<void> logout() async {
_updateState(const AuthState.unauthenticated());
}
void _updateState(AuthState newState) {
_currentState = newState;
_authStateController.add(newState);
}
void dispose() {
_authStateController.close();
}
}

View File

@@ -0,0 +1,86 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/auth_state.dart';
import '../models/login_request.dart';
import '../models/user_info.dart';
/// Service d'authentification ultra-simple sans aucune dépendance externe
class UltraSimpleAuthService {
final _authStateController = StreamController<AuthState>.broadcast();
AuthState _currentState = const AuthState.unknown();
Stream<AuthState> get authStateStream => _authStateController.stream;
AuthState get currentState => _currentState;
bool get isAuthenticated => _currentState.isAuthenticated;
UserInfo? get currentUser => _currentState.user;
Future<void> initialize() async {
_updateState(const AuthState.checking());
// Simuler une vérification
await Future.delayed(const Duration(seconds: 2));
_updateState(const AuthState.unauthenticated());
}
Future<void> login(LoginRequest request) async {
_updateState(_currentState.copyWith(isLoading: true));
try {
// Simulation d'appel API
await Future.delayed(const Duration(seconds: 1));
// Vérification simple pour la démo
if (request.email == 'admin@unionflow.dev' && request.password == 'admin123') {
final user = UserInfo(
id: '1',
email: request.email,
firstName: 'Admin',
lastName: 'UnionFlow',
role: 'admin',
isActive: true,
);
_updateState(AuthState.authenticated(
user: user,
accessToken: 'fake_access_token_${DateTime.now().millisecondsSinceEpoch}',
refreshToken: 'fake_refresh_token_${DateTime.now().millisecondsSinceEpoch}',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
));
} else if (request.email == 'president@lions.org' && request.password == 'admin123') {
final user = UserInfo(
id: '2',
email: request.email,
firstName: 'Jean',
lastName: 'Dupont',
role: 'président',
isActive: true,
);
_updateState(AuthState.authenticated(
user: user,
accessToken: 'fake_access_token_${DateTime.now().millisecondsSinceEpoch}',
refreshToken: 'fake_refresh_token_${DateTime.now().millisecondsSinceEpoch}',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
));
} else {
throw Exception('Identifiants invalides');
}
} catch (e) {
_updateState(AuthState.error(e.toString()));
}
}
Future<void> logout() async {
_updateState(const AuthState.unauthenticated());
}
void _updateState(AuthState newState) {
_currentState = newState;
_authStateController.add(newState);
}
void dispose() {
_authStateController.close();
}
}

View File

@@ -0,0 +1,117 @@
import 'dart:convert';
import '../models/login_response.dart';
import '../models/user_info.dart';
/// Service de stockage en mémoire des tokens (temporaire pour contourner Java 21)
class MemoryTokenStorage {
static final MemoryTokenStorage _instance = MemoryTokenStorage._internal();
factory MemoryTokenStorage() => _instance;
MemoryTokenStorage._internal();
// Stockage en mémoire
final Map<String, String> _storage = {};
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
static const String _userInfoKey = 'user_info';
static const String _expiresAtKey = 'expires_at';
static const String _refreshExpiresAtKey = 'refresh_expires_at';
/// Sauvegarde les données d'authentification
Future<void> saveAuthData(LoginResponse loginResponse) async {
try {
_storage[_accessTokenKey] = loginResponse.accessToken;
_storage[_refreshTokenKey] = loginResponse.refreshToken;
_storage[_userInfoKey] = jsonEncode(loginResponse.user.toJson());
_storage[_expiresAtKey] = loginResponse.expiresAt.toIso8601String();
_storage[_refreshExpiresAtKey] = loginResponse.refreshExpiresAt.toIso8601String();
} catch (e) {
throw StorageException('Erreur lors de la sauvegarde des données d\'authentification: $e');
}
}
/// Récupère le token d'accès
Future<String?> getAccessToken() async {
return _storage[_accessTokenKey];
}
/// Récupère le refresh token
Future<String?> getRefreshToken() async {
return _storage[_refreshTokenKey];
}
/// Récupère les informations utilisateur
Future<UserInfo?> getUserInfo() async {
try {
final userJson = _storage[_userInfoKey];
if (userJson == null) return null;
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
return UserInfo.fromJson(userMap);
} catch (e) {
throw StorageException('Erreur lors de la récupération des informations utilisateur: $e');
}
}
/// Récupère la date d'expiration du token d'accès
Future<DateTime?> getTokenExpirationDate() async {
try {
final expiresAtString = _storage[_expiresAtKey];
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration: $e');
}
}
/// Récupère la date d'expiration du refresh token
Future<DateTime?> getRefreshTokenExpirationDate() async {
try {
final expiresAtString = _storage[_refreshExpiresAtKey];
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration du refresh token: $e');
}
}
/// Vérifie si l'utilisateur est authentifié
Future<bool> hasValidToken() async {
final token = await getAccessToken();
if (token == null) return false;
final expirationDate = await getTokenExpirationDate();
if (expirationDate == null) return false;
return DateTime.now().isBefore(expirationDate);
}
/// Efface toutes les données d'authentification
Future<void> clearAll() async {
_storage.clear();
}
/// Met à jour uniquement les tokens
Future<void> updateTokens({
required String accessToken,
required String refreshToken,
required DateTime expiresAt,
required DateTime refreshExpiresAt,
}) async {
_storage[_accessTokenKey] = accessToken;
_storage[_refreshTokenKey] = refreshToken;
_storage[_expiresAtKey] = expiresAt.toIso8601String();
_storage[_refreshExpiresAtKey] = refreshExpiresAt.toIso8601String();
}
}
/// Exception personnalisée pour les erreurs de stockage
class StorageException implements Exception {
final String message;
StorageException(this.message);
@override
String toString() => 'StorageException: $message';
}

View File

@@ -0,0 +1,246 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:injectable/injectable.dart';
import '../models/login_response.dart';
import '../models/user_info.dart';
/// Service de stockage sécurisé des tokens d'authentification
@singleton
class SecureTokenStorage {
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
static const String _userInfoKey = 'user_info';
static const String _expiresAtKey = 'expires_at';
static const String _refreshExpiresAtKey = 'refresh_expires_at';
static const String _biometricEnabledKey = 'biometric_enabled';
// Utilise SharedPreferences temporairement pour Android
Future<SharedPreferences> get _prefs => SharedPreferences.getInstance();
/// Sauvegarde les données d'authentification
Future<void> saveAuthData(LoginResponse loginResponse) async {
try {
final prefs = await _prefs;
await Future.wait([
prefs.setString(_accessTokenKey, loginResponse.accessToken),
prefs.setString(_refreshTokenKey, loginResponse.refreshToken),
prefs.setString(_userInfoKey, jsonEncode(loginResponse.user.toJson())),
prefs.setString(_expiresAtKey, loginResponse.expiresAt.toIso8601String()),
prefs.setString(_refreshExpiresAtKey, loginResponse.refreshExpiresAt.toIso8601String()),
]);
} catch (e) {
throw StorageException('Erreur lors de la sauvegarde des données d\'authentification: $e');
}
}
/// Récupère le token d'accès
Future<String?> getAccessToken() async {
try {
final prefs = await _prefs;
return prefs.getString(_accessTokenKey);
} catch (e) {
throw StorageException('Erreur lors de la récupération du token d\'accès: $e');
}
}
/// Récupère le refresh token
Future<String?> getRefreshToken() async {
try {
final prefs = await _prefs;
return prefs.getString(_refreshTokenKey);
} catch (e) {
throw StorageException('Erreur lors de la récupération du refresh token: $e');
}
}
/// Récupère les informations utilisateur
Future<UserInfo?> getUserInfo() async {
try {
final prefs = await _prefs;
final userJson = prefs.getString(_userInfoKey);
if (userJson == null) return null;
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
return UserInfo.fromJson(userMap);
} catch (e) {
throw StorageException('Erreur lors de la récupération des informations utilisateur: $e');
}
}
/// Récupère la date d'expiration du token d'accès
Future<DateTime?> getTokenExpirationDate() async {
try {
final prefs = await _prefs;
final expiresAtString = prefs.getString(_expiresAtKey);
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration: $e');
}
}
/// Récupère la date d'expiration du refresh token
Future<DateTime?> getRefreshTokenExpirationDate() async {
try {
final expiresAtString = await _storage.read(key: _refreshExpiresAtKey);
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration du refresh token: $e');
}
}
/// Récupère toutes les données d'authentification
Future<LoginResponse?> getAuthData() async {
try {
final results = await Future.wait([
getAccessToken(),
getRefreshToken(),
getUserInfo(),
getTokenExpirationDate(),
getRefreshTokenExpirationDate(),
]);
final accessToken = results[0] as String?;
final refreshToken = results[1] as String?;
final userInfo = results[2] as UserInfo?;
final expiresAt = results[3] as DateTime?;
final refreshExpiresAt = results[4] as DateTime?;
if (accessToken == null ||
refreshToken == null ||
userInfo == null ||
expiresAt == null ||
refreshExpiresAt == null) {
return null;
}
return LoginResponse(
accessToken: accessToken,
refreshToken: refreshToken,
tokenType: 'Bearer',
expiresAt: expiresAt,
refreshExpiresAt: refreshExpiresAt,
user: userInfo,
);
} catch (e) {
throw StorageException('Erreur lors de la récupération des données d\'authentification: $e');
}
}
/// Met à jour le token d'accès
Future<void> updateAccessToken(String accessToken, DateTime expiresAt) async {
try {
await Future.wait([
_storage.write(key: _accessTokenKey, value: accessToken),
_storage.write(key: _expiresAtKey, value: expiresAt.toIso8601String()),
]);
} catch (e) {
throw StorageException('Erreur lors de la mise à jour du token d\'accès: $e');
}
}
/// Vérifie si les données d'authentification existent
Future<bool> hasAuthData() async {
try {
final accessToken = await _storage.read(key: _accessTokenKey);
final refreshToken = await _storage.read(key: _refreshTokenKey);
return accessToken != null && refreshToken != null;
} catch (e) {
return false;
}
}
/// Vérifie si les tokens sont expirés
Future<bool> areTokensExpired() async {
try {
final expiresAt = await getTokenExpirationDate();
final refreshExpiresAt = await getRefreshTokenExpirationDate();
if (expiresAt == null || refreshExpiresAt == null) return true;
final now = DateTime.now();
return refreshExpiresAt.isBefore(now);
} catch (e) {
return true;
}
}
/// Vérifie si le token d'accès expire bientôt
Future<bool> isAccessTokenExpiringSoon({int minutes = 5}) async {
try {
final expiresAt = await getTokenExpirationDate();
if (expiresAt == null) return true;
final threshold = DateTime.now().add(Duration(minutes: minutes));
return expiresAt.isBefore(threshold);
} catch (e) {
return true;
}
}
/// Efface toutes les données d'authentification
Future<void> clearAuthData() async {
try {
await Future.wait([
_storage.delete(key: _accessTokenKey),
_storage.delete(key: _refreshTokenKey),
_storage.delete(key: _userInfoKey),
_storage.delete(key: _expiresAtKey),
_storage.delete(key: _refreshExpiresAtKey),
]);
} catch (e) {
throw StorageException('Erreur lors de l\'effacement des données d\'authentification: $e');
}
}
/// Active/désactive l'authentification biométrique
Future<void> setBiometricEnabled(bool enabled) async {
try {
await _storage.write(key: _biometricEnabledKey, value: enabled.toString());
} catch (e) {
throw StorageException('Erreur lors de la configuration biométrique: $e');
}
}
/// Vérifie si l'authentification biométrique est activée
Future<bool> isBiometricEnabled() async {
try {
final enabled = await _storage.read(key: _biometricEnabledKey);
return enabled == 'true';
} catch (e) {
return false;
}
}
/// Efface toutes les données stockées
Future<void> clearAll() async {
try {
await _storage.deleteAll();
} catch (e) {
throw StorageException('Erreur lors de l\'effacement de toutes les données: $e');
}
}
/// Vérifie si le stockage sécurisé est disponible
Future<bool> isAvailable() async {
try {
await _storage.containsKey(key: 'test');
return true;
} catch (e) {
return false;
}
}
}
/// Exception liée au stockage
class StorageException implements Exception {
final String message;
const StorageException(this.message);
@override
String toString() => 'StorageException: $message';
}

View File

@@ -0,0 +1,74 @@
class AppConstants {
// API Configuration
static const String baseUrl = 'http://localhost:8099'; // Backend UnionFlow
static const String apiVersion = '/api/v1';
// Timeout
static const Duration connectTimeout = Duration(seconds: 30);
static const Duration receiveTimeout = Duration(seconds: 30);
// Storage Keys
static const String authTokenKey = 'auth_token';
static const String refreshTokenKey = 'refresh_token';
static const String userDataKey = 'user_data';
static const String appSettingsKey = 'app_settings';
// API Endpoints
static const String loginEndpoint = '/auth/login';
static const String refreshEndpoint = '/auth/refresh';
static const String membresEndpoint = '/membres';
static const String cotisationsEndpoint = '/finance/cotisations';
static const String evenementsEndpoint = '/evenements';
static const String statistiquesEndpoint = '/statistiques';
// App Configuration
static const String appName = 'UnionFlow';
static const String appVersion = '2.0.0';
static const int maxRetryAttempts = 3;
// Pagination
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
// File Upload
static const int maxFileSize = 10 * 1024 * 1024; // 10MB
static const List<String> allowedImageTypes = ['jpg', 'jpeg', 'png', 'gif'];
static const List<String> allowedDocumentTypes = ['pdf', 'doc', 'docx'];
// Chart Colors
static const List<String> chartColors = [
'#2196F3', '#4CAF50', '#FF9800', '#F44336',
'#9C27B0', '#00BCD4', '#8BC34A', '#FFEB3B'
];
}
class ApiEndpoints {
// Authentication
static const String login = '/auth/login';
static const String logout = '/auth/logout';
static const String register = '/auth/register';
static const String refreshToken = '/auth/refresh';
static const String forgotPassword = '/auth/forgot-password';
// Membres
static const String membres = '/membres';
static const String membreProfile = '/membres/profile';
static const String membreSearch = '/membres/search';
static const String membreStats = '/membres/statistiques';
// Cotisations
static const String cotisations = '/finance/cotisations';
static const String cotisationsPay = '/finance/cotisations/payer';
static const String cotisationsHistory = '/finance/cotisations/historique';
static const String cotisationsStats = '/finance/cotisations/statistiques';
// Événements
static const String evenements = '/evenements';
static const String evenementParticipants = '/evenements/{id}/participants';
static const String evenementDocuments = '/evenements/{id}/documents';
// Dashboard
static const String dashboardStats = '/dashboard/statistiques';
static const String dashboardCharts = '/dashboard/charts';
static const String dashboardNotifications = '/dashboard/notifications';
}

View File

@@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// InjectableConfigGenerator
// **************************************************************************
// ignore_for_file: unnecessary_lambdas
// ignore_for_file: lines_longer_than_80_chars
// coverage:ignore-file
import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2;
import '../auth/bloc/auth_bloc.dart' as _i7;
import '../auth/services/auth_api_service.dart' as _i4;
import '../auth/services/auth_service.dart' as _i6;
import '../auth/storage/secure_token_storage.dart' as _i3;
import '../network/auth_interceptor.dart' as _i5;
import '../network/dio_client.dart' as _i8;
extension GetItInjectableX on _i1.GetIt {
// initializes the registration of main-scope dependencies inside of GetIt
Future<_i1.GetIt> init({
String? environment,
_i2.EnvironmentFilter? environmentFilter,
}) async {
final gh = _i2.GetItHelper(
this,
environment,
environmentFilter,
);
gh.singleton<_i3.SecureTokenStorage>(() => _i3.SecureTokenStorage());
gh.singleton<_i8.DioClient>(() => _i8.DioClient());
gh.singleton<_i5.AuthInterceptor>(() => _i5.AuthInterceptor(gh<_i3.SecureTokenStorage>()));
gh.singleton<_i4.AuthApiService>(() => _i4.AuthApiService(gh<_i8.DioClient>()));
gh.singleton<_i6.AuthService>(() => _i6.AuthService(
gh<_i3.SecureTokenStorage>(),
gh<_i4.AuthApiService>(),
gh<_i5.AuthInterceptor>(),
gh<_i8.DioClient>(),
));
gh.singleton<_i7.AuthBloc>(() => _i7.AuthBloc(gh<_i6.AuthService>()));
return this;
}
}

View File

@@ -0,0 +1,19 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart';
/// Instance globale de GetIt pour l'injection de dépendances
final GetIt getIt = GetIt.instance;
/// Configure l'injection de dépendances
@InjectableInit()
Future<void> configureDependencies() async {
await getIt.init();
}
/// Réinitialise les dépendances (utile pour les tests)
Future<void> resetDependencies() async {
await getIt.reset();
await configureDependencies();
}

View File

@@ -0,0 +1,122 @@
import 'package:equatable/equatable.dart';
abstract class Failure extends Equatable {
const Failure({required this.message, this.code});
final String message;
final String? code;
@override
List<Object?> get props => [message, code];
}
class ServerFailure extends Failure {
const ServerFailure({
required super.message,
super.code,
this.statusCode,
});
final int? statusCode;
@override
List<Object?> get props => [message, code, statusCode];
}
class NetworkFailure extends Failure {
const NetworkFailure({
required super.message,
super.code = 'NETWORK_ERROR',
});
}
class AuthFailure extends Failure {
const AuthFailure({
required super.message,
super.code = 'AUTH_ERROR',
});
}
class ValidationFailure extends Failure {
const ValidationFailure({
required super.message,
super.code = 'VALIDATION_ERROR',
this.field,
});
final String? field;
@override
List<Object?> get props => [message, code, field];
}
class CacheFailure extends Failure {
const CacheFailure({
required super.message,
super.code = 'CACHE_ERROR',
});
}
class UnknownFailure extends Failure {
const UnknownFailure({
required super.message,
super.code = 'UNKNOWN_ERROR',
});
}
// Extension pour convertir les exceptions en failures
extension ExceptionToFailure on Exception {
Failure toFailure() {
if (this is NetworkException) {
final ex = this as NetworkException;
return NetworkFailure(message: ex.message);
} else if (this is ServerException) {
final ex = this as ServerException;
return ServerFailure(
message: ex.message,
statusCode: ex.statusCode,
);
} else if (this is AuthException) {
final ex = this as AuthException;
return AuthFailure(message: ex.message);
} else if (this is ValidationException) {
final ex = this as ValidationException;
return ValidationFailure(
message: ex.message,
field: ex.field,
);
} else if (this is CacheException) {
final ex = this as CacheException;
return CacheFailure(message: ex.message);
}
return UnknownFailure(message: toString());
}
}
// Exceptions personnalisées
class NetworkException implements Exception {
const NetworkException(this.message);
final String message;
}
class ServerException implements Exception {
const ServerException(this.message, {this.statusCode});
final String message;
final int? statusCode;
}
class AuthException implements Exception {
const AuthException(this.message);
final String message;
}
class ValidationException implements Exception {
const ValidationException(this.message, {this.field});
final String message;
final String? field;
}
class CacheException implements Exception {
const CacheException(this.message);
final String message;
}

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

View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
/// Utilitaires pour rendre l'app responsive
class ResponsiveUtils {
static late MediaQueryData _mediaQueryData;
static late double screenWidth;
static late double screenHeight;
static late double blockSizeHorizontal;
static late double blockSizeVertical;
static late double safeAreaHorizontal;
static late double safeAreaVertical;
static late double safeBlockHorizontal;
static late double safeBlockVertical;
static late double textScaleFactor;
static void init(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData.size.width;
screenHeight = _mediaQueryData.size.height;
blockSizeHorizontal = screenWidth / 100;
blockSizeVertical = screenHeight / 100;
final safeAreaPadding = _mediaQueryData.padding;
safeAreaHorizontal = screenWidth - safeAreaPadding.left - safeAreaPadding.right;
safeAreaVertical = screenHeight - safeAreaPadding.top - safeAreaPadding.bottom;
safeBlockHorizontal = safeAreaHorizontal / 100;
safeBlockVertical = safeAreaVertical / 100;
textScaleFactor = _mediaQueryData.textScaleFactor;
}
// Responsive width
static double wp(double percentage) => blockSizeHorizontal * percentage;
// Responsive height
static double hp(double percentage) => blockSizeVertical * percentage;
// Responsive font size (basé sur la largeur)
static double fs(double percentage) => safeBlockHorizontal * percentage;
// Responsive spacing
static double sp(double percentage) => safeBlockHorizontal * percentage;
// Responsive padding/margin
static EdgeInsets paddingAll(double percentage) =>
EdgeInsets.all(sp(percentage));
static EdgeInsets paddingSymmetric({double? horizontal, double? vertical}) =>
EdgeInsets.symmetric(
horizontal: horizontal != null ? sp(horizontal) : 0,
vertical: vertical != null ? hp(vertical) : 0,
);
static EdgeInsets paddingOnly({
double? left,
double? top,
double? right,
double? bottom,
}) =>
EdgeInsets.only(
left: left != null ? sp(left) : 0,
top: top != null ? hp(top) : 0,
right: right != null ? sp(right) : 0,
bottom: bottom != null ? hp(bottom) : 0,
);
// Adaptive values based on screen size
static double adaptive({
required double small, // < 600px (phones)
required double medium, // 600-900px (tablets)
required double large, // > 900px (desktop)
}) {
if (screenWidth < 600) return small;
if (screenWidth < 900) return medium;
return large;
}
// Check device type
static bool get isMobile => screenWidth < 600;
static bool get isTablet => screenWidth >= 600 && screenWidth < 900;
static bool get isDesktop => screenWidth >= 900;
// Responsive border radius
static BorderRadius borderRadius(double percentage) =>
BorderRadius.circular(sp(percentage));
// Responsive icon size
static double iconSize(double percentage) =>
adaptive(
small: sp(percentage),
medium: sp(percentage * 0.9),
large: sp(percentage * 0.8),
);
}
// Extension pour faciliter l'utilisation
extension ResponsiveExtension on num {
// Width percentage
double get wp => ResponsiveUtils.wp(toDouble());
// Height percentage
double get hp => ResponsiveUtils.hp(toDouble());
// Font size
double get fs => ResponsiveUtils.fs(toDouble());
// Spacing
double get sp => ResponsiveUtils.sp(toDouble());
}