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