import 'package:postgres/postgres.dart'; import '../../application/application.dart'; import '../../domain/domain.dart'; final class PostgresUserRepository implements UserRepository { const PostgresUserRepository(this.connection); final Connection connection; @override Future findByEmail(String email) async { final result = await connection.execute( Sql.named(''' SELECT id, email, password_hash, display_name, created_at, updated_at, disabled_at FROM users WHERE email = @email LIMIT 1 '''), parameters: {'email': email.trim().toLowerCase()}, ); return result.isEmpty ? null : _userFromRow(result.single); } @override Future findById(String id) async { final result = await connection.execute( Sql.named(''' SELECT id, email, password_hash, display_name, created_at, updated_at, disabled_at FROM users WHERE id = @id LIMIT 1 '''), parameters: {'id': id}, ); return result.isEmpty ? null : _userFromRow(result.single); } @override Future insert(UserAccount user) async { await connection.execute( Sql.named(''' INSERT INTO users ( id, email, password_hash, display_name, created_at, updated_at, disabled_at ) VALUES ( @id, @email, @password_hash, @display_name, @created_at, @updated_at, @disabled_at ) '''), parameters: { 'id': user.id, 'email': user.email, 'password_hash': user.passwordHash, 'display_name': user.displayName, 'created_at': user.createdAt, 'updated_at': user.updatedAt, 'disabled_at': user.disabledAt, }, ); } @override Future updatePasswordHash({ required String userId, required String passwordHash, required DateTime updatedAt, }) async { await connection.execute( Sql.named(''' UPDATE users SET password_hash = @password_hash, updated_at = @updated_at WHERE id = @user_id '''), parameters: { 'user_id': userId, 'password_hash': passwordHash, 'updated_at': updatedAt.toUtc(), }, ); } } final class PostgresAuthSessionRepository implements AuthSessionRepository { const PostgresAuthSessionRepository(this.connection); final Connection connection; @override Future insert(AuthSession session) async { await connection.execute( Sql.named(''' INSERT INTO auth_sessions ( id, user_id, token_hash, issued_at, expires_at, revoked_at, user_agent, device_label ) VALUES ( @id, @user_id, @token_hash, @issued_at, @expires_at, @revoked_at, @user_agent, @device_label ) '''), parameters: { 'id': session.id, 'user_id': session.userId, 'token_hash': session.tokenHash, 'issued_at': session.issuedAt, 'expires_at': session.expiresAt, 'revoked_at': session.revokedAt, 'user_agent': session.userAgent, 'device_label': session.deviceLabel, }, ); } @override Future findByTokenHash(String tokenHash) async { final result = await connection.execute( Sql.named(''' SELECT id, user_id, token_hash, issued_at, expires_at, revoked_at, user_agent, device_label FROM auth_sessions WHERE token_hash = @token_hash LIMIT 1 '''), parameters: {'token_hash': tokenHash}, ); return result.isEmpty ? null : _authSessionFromRow(result.single); } @override Future revoke({ required String sessionId, required DateTime revokedAt, }) async { await connection.execute( Sql.named(''' UPDATE auth_sessions SET revoked_at = @revoked_at WHERE id = @id AND revoked_at IS NULL '''), parameters: {'id': sessionId, 'revoked_at': revokedAt.toUtc()}, ); } } UserAccount _userFromRow(dynamic row) { final values = row.toColumnMap() as Map; return UserAccount( id: _stringValue(values['id']), email: _stringValue(values['email']), passwordHash: _stringValue(values['password_hash']), displayName: values['display_name'] as String?, createdAt: _dateTimeValue(values['created_at']), updatedAt: _dateTimeValue(values['updated_at']), disabledAt: _nullableDateTimeValue(values['disabled_at']), ); } AuthSession _authSessionFromRow(dynamic row) { final values = row.toColumnMap() as Map; return AuthSession( id: _stringValue(values['id']), userId: _stringValue(values['user_id']), tokenHash: _stringValue(values['token_hash']), issuedAt: _dateTimeValue(values['issued_at']), expiresAt: _dateTimeValue(values['expires_at']), revokedAt: _nullableDateTimeValue(values['revoked_at']), userAgent: values['user_agent'] as String?, deviceLabel: values['device_label'] as String?, ); } String _stringValue(Object? value) { if (value == null) { throw const FormatException('Expected non-null string value.'); } return value.toString(); } DateTime _dateTimeValue(Object? value) { final result = _nullableDateTimeValue(value); if (result == null) { throw const FormatException('Expected non-null DateTime value.'); } return result; } DateTime? _nullableDateTimeValue(Object? value) { if (value == null) { return null; } if (value is DateTime) { return value.toUtc(); } return DateTime.parse(value.toString()).toUtc(); }