feat(online): session compte, stockage sécurisé et adapter API (ticket #64)
Ajoute l'adapter API distant (infrastructure/remote/auth_api.dart, http_api_client.dart) et le stockage sécurisé du token d'auth (infrastructure/security/secure_storage_auth_token_store.dart), câblés dans app_bootstrap.dart. Étend le modèle Drift (migration schemaVersion 9→10) et la couche application (ports, use cases, entités) pour la session de compte. flutter pub get OK (ajout http, flutter_secure_storage), build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 109/109 tests verts, build APK debug validé. Premier ticket du chantier "Ajouter les features serveur au client" (#63). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -5,6 +5,107 @@ import 'ports.dart';
|
||||
|
||||
const Object _useCaseUnchanged = Object();
|
||||
|
||||
final class AuthUseCases {
|
||||
const AuthUseCases({
|
||||
required this.tokenStore,
|
||||
required this.accountRepository,
|
||||
required this.remoteAuthApi,
|
||||
required this.clock,
|
||||
required this.ids,
|
||||
});
|
||||
|
||||
final AuthTokenStore tokenStore;
|
||||
final OnlineAccountRepository accountRepository;
|
||||
final RemoteAuthApi remoteAuthApi;
|
||||
final Clock clock;
|
||||
final IdGenerator ids;
|
||||
|
||||
Future<UserAccountSession> register({
|
||||
required String email,
|
||||
required String password,
|
||||
String? displayName,
|
||||
}) async {
|
||||
_validateAuthInput(email: email, password: password);
|
||||
final result = await remoteAuthApi.register(
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
displayName: displayName,
|
||||
);
|
||||
return _persistAuthenticatedSession(result, displayName: displayName);
|
||||
}
|
||||
|
||||
Future<UserAccountSession> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
_validateAuthInput(email: email, password: password);
|
||||
final result = await remoteAuthApi.login(
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
);
|
||||
return _persistAuthenticatedSession(result);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
final token = await tokenStore.readToken();
|
||||
if (token != null) {
|
||||
try {
|
||||
await remoteAuthApi.logout(token);
|
||||
} on RemoteAuthException {
|
||||
// Local logout must not depend on remote reachability.
|
||||
}
|
||||
}
|
||||
await tokenStore.clearToken();
|
||||
final session = await accountRepository.currentSession();
|
||||
if (session != null) {
|
||||
final now = clock.now();
|
||||
await accountRepository.saveSession(
|
||||
session.copyWith(isLoggedIn: false, updatedAt: now),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserAccountSession?> currentSession() async {
|
||||
final session = await accountRepository.currentSession();
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
final token = await tokenStore.readToken();
|
||||
if (token != null || !session.isLoggedIn) {
|
||||
return session;
|
||||
}
|
||||
final updated = session.copyWith(isLoggedIn: false, updatedAt: clock.now());
|
||||
await accountRepository.saveSession(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<UserAccountSession> _persistAuthenticatedSession(
|
||||
RemoteAuthResult result, {
|
||||
String? displayName,
|
||||
}) async {
|
||||
await tokenStore.saveToken(result.token, result.expiresAt);
|
||||
final now = clock.now();
|
||||
final existing = await accountRepository.currentSession();
|
||||
final session = UserAccountSession(
|
||||
id: existing?.id ?? ids.newId(),
|
||||
serverUserId:
|
||||
existing != null &&
|
||||
existing.email.trim().toLowerCase() ==
|
||||
result.email.trim().toLowerCase()
|
||||
? existing.serverUserId
|
||||
: result.userId,
|
||||
email: result.email,
|
||||
displayName: displayName ?? existing?.displayName,
|
||||
isLoggedIn: true,
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
lastAuthenticatedAt: now,
|
||||
);
|
||||
await accountRepository.saveSession(session);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
final class ExerciseUseCases {
|
||||
const ExerciseUseCases({
|
||||
required this.repository,
|
||||
@ -2264,6 +2365,19 @@ void _validateExerciseSteps(List<ExerciseStep> steps) {
|
||||
}
|
||||
}
|
||||
|
||||
void _validateAuthInput({required String email, required String password}) {
|
||||
final normalizedEmail = email.trim();
|
||||
final hasBasicEmailShape = RegExp(
|
||||
r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
|
||||
).hasMatch(normalizedEmail);
|
||||
if (!hasBasicEmailShape) {
|
||||
throw const DomainException('Email format is invalid.');
|
||||
}
|
||||
if (password.length < 8) {
|
||||
throw const DomainException('Password must contain at least 8 characters.');
|
||||
}
|
||||
}
|
||||
|
||||
void _validateExerciseDefaultTargets({
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
|
||||
Reference in New Issue
Block a user