initial commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../../features/auth/domain/auth_user.dart';
|
||||
import 'auth_repository.dart';
|
||||
|
||||
/// État d'authentification de l'application.
|
||||
class AuthState {
|
||||
const AuthState({
|
||||
this.user,
|
||||
this.accessToken,
|
||||
this.refreshToken,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
final AuthUser? user;
|
||||
final String? accessToken;
|
||||
final String? refreshToken;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
bool get isAuthenticated => accessToken != null && accessToken!.isNotEmpty;
|
||||
|
||||
AuthState copyWith({
|
||||
AuthUser? user,
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
bool clearUser = false,
|
||||
}) {
|
||||
return AuthState(
|
||||
user: clearUser ? null : (user ?? this.user),
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
refreshToken: refreshToken ?? this.refreshToken,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gestionnaire d'état Riverpod pour l'authentification.
|
||||
class AuthNotifier extends StateNotifier<AuthState> {
|
||||
AuthNotifier(this._repository) : super(const AuthState()) {
|
||||
_restoreSession();
|
||||
}
|
||||
|
||||
final AuthRepository _repository;
|
||||
|
||||
Future<void> _restoreSession() async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final stored = await _repository.loadStoredTokens();
|
||||
if (stored == null) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
accessToken: stored.accessToken,
|
||||
refreshToken: stored.refreshToken,
|
||||
);
|
||||
|
||||
final user = await _repository.fetchCurrentUser();
|
||||
state = state.copyWith(user: user, isLoading: false);
|
||||
} catch (_) {
|
||||
final refreshed = await _repository.refresh();
|
||||
if (refreshed != null) {
|
||||
final user = await _repository.fetchCurrentUser();
|
||||
state = state.copyWith(
|
||||
accessToken: refreshed.accessToken,
|
||||
refreshToken: refreshed.refreshToken,
|
||||
user: user,
|
||||
isLoading: false,
|
||||
);
|
||||
} else {
|
||||
await _repository.clearStoredTokens();
|
||||
state = const AuthState(isLoading: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> login(String email, String password) async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final tokens = await _repository.login(email: email, password: password);
|
||||
final user = tokens.user ?? await _repository.fetchCurrentUser();
|
||||
|
||||
state = state.copyWith(
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
user: user,
|
||||
isLoading: false,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Connexion impossible. Vérifiez vos identifiants.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> register({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
required String password,
|
||||
String? phone,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final tokens = await _repository.register(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
email: email,
|
||||
password: password,
|
||||
phone: phone,
|
||||
);
|
||||
final user = tokens.user ?? await _repository.fetchCurrentUser();
|
||||
|
||||
state = state.copyWith(
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
user: user,
|
||||
isLoading: false,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Inscription impossible. L\'email est peut-être déjà utilisé.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _repository.logout();
|
||||
state = const AuthState();
|
||||
}
|
||||
}
|
||||
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
// Client dédié à l'auth pour éviter une dépendance circulaire avec apiClientProvider.
|
||||
return AuthRepository(apiClient: ApiClient(baseUrl: kApiBaseUrl));
|
||||
});
|
||||
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
||||
return AuthNotifier(ref.watch(authRepositoryProvider));
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../api/api_endpoints.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../config/secure_storage_config.dart';
|
||||
import '../../features/auth/domain/auth_user.dart';
|
||||
|
||||
/// Dépôt d'authentification — login, inscription, refresh et logout.
|
||||
class AuthRepository {
|
||||
AuthRepository({
|
||||
required ApiClient apiClient,
|
||||
FlutterSecureStorage? secureStorage,
|
||||
}) : _apiClient = apiClient,
|
||||
_secureStorage = secureStorage ?? laverieSecureStorage;
|
||||
|
||||
final ApiClient _apiClient;
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
Future<AuthTokens> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.authLogin,
|
||||
data: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
|
||||
final tokens = AuthTokens.fromJson(response.data as Map<String, dynamic>);
|
||||
await _persistTokens(tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
Future<AuthTokens> register({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
required String password,
|
||||
String? phone,
|
||||
}) async {
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.authRegister,
|
||||
data: {
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'email': email,
|
||||
'password': password,
|
||||
if (phone != null) 'phone': phone,
|
||||
},
|
||||
);
|
||||
|
||||
final tokens = AuthTokens.fromJson(response.data as Map<String, dynamic>);
|
||||
await _persistTokens(tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
Future<AuthTokens?> refresh() async {
|
||||
final refreshToken = await _secureStorage.read(key: AuthStorageKeys.refreshToken);
|
||||
if (refreshToken == null || refreshToken.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.authRefresh,
|
||||
data: {'refresh_token': refreshToken},
|
||||
);
|
||||
|
||||
final tokens = AuthTokens.fromJson(response.data as Map<String, dynamic>);
|
||||
await _persistTokens(tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
Future<AuthUser?> fetchCurrentUser() async {
|
||||
final response = await _apiClient.get(ApiEndpoints.authMe);
|
||||
final data = response.data;
|
||||
|
||||
if (data is Map<String, dynamic>) {
|
||||
if (data.containsKey('data')) {
|
||||
return AuthUser.fromJson(data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
return AuthUser.fromJson(data);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await _apiClient.post(ApiEndpoints.authLogout);
|
||||
} catch (_) {
|
||||
// Déconnexion locale même si l'API est injoignable.
|
||||
}
|
||||
await clearStoredTokens();
|
||||
}
|
||||
|
||||
Future<AuthTokens?> loadStoredTokens() async {
|
||||
final accessToken = await _secureStorage.read(key: AuthStorageKeys.accessToken);
|
||||
final refreshToken = await _secureStorage.read(key: AuthStorageKeys.refreshToken);
|
||||
|
||||
if (accessToken == null || refreshToken == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuthTokens(accessToken: accessToken, refreshToken: refreshToken);
|
||||
}
|
||||
|
||||
Future<void> clearStoredTokens() async {
|
||||
await _secureStorage.delete(key: AuthStorageKeys.accessToken);
|
||||
await _secureStorage.delete(key: AuthStorageKeys.refreshToken);
|
||||
}
|
||||
|
||||
Future<void> _persistTokens(AuthTokens tokens) async {
|
||||
await _secureStorage.write(
|
||||
key: AuthStorageKeys.accessToken,
|
||||
value: tokens.accessToken,
|
||||
);
|
||||
await _secureStorage.write(
|
||||
key: AuthStorageKeys.refreshToken,
|
||||
value: tokens.refreshToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user