feat(server): auth comptes utilisateurs et tokens API (ticket #48)
Ajoute la couche application (ports, use cases), les entités du domaine, l'endpoint api/auth_api.dart (register/login/logout) et son câblage dans le router, le hashing de mot de passe et le service de token (infrastructure/security), et l'adapter Postgres des repositories d'auth s'appuyant sur la table users du ticket #49. dart pub get OK, dart analyze clean, dart test 10/10 vert (test PostgreSQL réel skip faute de Docker disponible dans ce sandbox). Logique d'auth et middleware testés via repositories fake en mémoire ; les endpoints HTTP register/login/logout n'ont pas pu être exercés de bout en bout faute d'accès à un vrai PostgreSQL local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -4,3 +4,5 @@ Pure server domain entities and invariants.
|
||||
|
||||
This layer must not import Shelf, PostgreSQL adapters, Docker configuration or
|
||||
other infrastructure concerns.
|
||||
|
||||
Current entities: `UserAccount` and `AuthSession`.
|
||||
|
||||
1
server/lib/domain/domain.dart
Normal file
1
server/lib/domain/domain.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'entities.dart';
|
||||
97
server/lib/domain/entities.dart
Normal file
97
server/lib/domain/entities.dart
Normal file
@ -0,0 +1,97 @@
|
||||
final class DomainException implements Exception {
|
||||
const DomainException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
final class ValidationException extends DomainException {
|
||||
const ValidationException(super.message);
|
||||
}
|
||||
|
||||
final class EmailAlreadyTakenException extends DomainException {
|
||||
const EmailAlreadyTakenException() : super('Email already taken.');
|
||||
}
|
||||
|
||||
final class InvalidCredentialsException extends DomainException {
|
||||
const InvalidCredentialsException() : super('Invalid credentials.');
|
||||
}
|
||||
|
||||
final class UnauthorizedException extends DomainException {
|
||||
const UnauthorizedException() : super('Unauthorized.');
|
||||
}
|
||||
|
||||
final class UserAccount {
|
||||
UserAccount({
|
||||
required String id,
|
||||
required String email,
|
||||
required String passwordHash,
|
||||
this.displayName,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
DateTime? disabledAt,
|
||||
}) : id = _nonBlank(id, 'User id'),
|
||||
email = _nonBlank(email, 'Email').toLowerCase(),
|
||||
passwordHash = _nonBlank(passwordHash, 'Password hash'),
|
||||
createdAt = createdAt.toUtc(),
|
||||
updatedAt = updatedAt.toUtc(),
|
||||
disabledAt = disabledAt?.toUtc();
|
||||
|
||||
final String id;
|
||||
final String email;
|
||||
final String passwordHash;
|
||||
final String? displayName;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? disabledAt;
|
||||
|
||||
bool get isDisabled => disabledAt != null;
|
||||
}
|
||||
|
||||
final class AuthSession {
|
||||
AuthSession({
|
||||
required String id,
|
||||
required String userId,
|
||||
required String tokenHash,
|
||||
required DateTime issuedAt,
|
||||
required DateTime expiresAt,
|
||||
DateTime? revokedAt,
|
||||
this.userAgent,
|
||||
this.deviceLabel,
|
||||
}) : id = _nonBlank(id, 'Auth session id'),
|
||||
userId = _nonBlank(userId, 'User id'),
|
||||
tokenHash = _nonBlank(tokenHash, 'Token hash'),
|
||||
issuedAt = issuedAt.toUtc(),
|
||||
expiresAt = expiresAt.toUtc(),
|
||||
revokedAt = revokedAt?.toUtc() {
|
||||
if (!this.expiresAt.isAfter(this.issuedAt)) {
|
||||
throw const ValidationException(
|
||||
'Auth session expiration must be after issue time.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String tokenHash;
|
||||
final DateTime issuedAt;
|
||||
final DateTime expiresAt;
|
||||
final DateTime? revokedAt;
|
||||
final String? userAgent;
|
||||
final String? deviceLabel;
|
||||
|
||||
bool isActiveAt(DateTime now) {
|
||||
final instant = now.toUtc();
|
||||
return revokedAt == null && expiresAt.isAfter(instant);
|
||||
}
|
||||
}
|
||||
|
||||
String _nonBlank(String value, String label) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
throw ValidationException('$label must not be blank.');
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
Reference in New Issue
Block a user