Refactor runtime bootstrap and ship control flows
This commit is contained in:
199
apps/backend/Auth/Simulation/PostgresAuthRepository.cs
Normal file
199
apps/backend/Auth/Simulation/PostgresAuthRepository.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using Npgsql;
|
||||
|
||||
namespace SpaceGame.Api.Auth.Simulation;
|
||||
|
||||
public sealed class PostgresAuthRepository(NpgsqlDataSource dataSource) : IAuthRepository
|
||||
{
|
||||
public async Task<UserAccount?> FindUserByEmailAsync(string email, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
select id, email, password_hash, created_at_utc, roles
|
||||
from auth_users
|
||||
where email = $1
|
||||
""");
|
||||
command.Parameters.AddWithValue(email);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
return await reader.ReadAsync(cancellationToken) ? ReadUser(reader) : null;
|
||||
}
|
||||
|
||||
public async Task<UserAccount?> FindUserByIdAsync(Guid userId, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
select id, email, password_hash, created_at_utc, roles
|
||||
from auth_users
|
||||
where id = $1
|
||||
""");
|
||||
command.Parameters.AddWithValue(userId);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
return await reader.ReadAsync(cancellationToken) ? ReadUser(reader) : null;
|
||||
}
|
||||
|
||||
public async Task<UserAccount> CreateUserAsync(string email, string passwordHash, IReadOnlyCollection<string> roles, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var createdAtUtc = DateTimeOffset.UtcNow;
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
insert into auth_users (id, email, password_hash, created_at_utc, roles)
|
||||
values ($1, $2, $3, $4, $5)
|
||||
""");
|
||||
command.Parameters.AddWithValue(userId);
|
||||
command.Parameters.AddWithValue(email);
|
||||
command.Parameters.AddWithValue(passwordHash);
|
||||
command.Parameters.AddWithValue(createdAtUtc);
|
||||
command.Parameters.AddWithValue(roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray());
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
return new UserAccount(userId, email, passwordHash, createdAtUtc, roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
|
||||
public async Task<UserAccount> UpsertUserAsync(string email, string passwordHash, IReadOnlyCollection<string> roles, CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedRoles = roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray();
|
||||
var userId = Guid.NewGuid();
|
||||
var createdAtUtc = DateTimeOffset.UtcNow;
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
insert into auth_users (id, email, password_hash, created_at_utc, roles)
|
||||
values ($1, $2, $3, $4, $5)
|
||||
on conflict (email) do update
|
||||
set password_hash = excluded.password_hash,
|
||||
roles = excluded.roles
|
||||
returning id, email, password_hash, created_at_utc, roles
|
||||
""");
|
||||
command.Parameters.AddWithValue(userId);
|
||||
command.Parameters.AddWithValue(email);
|
||||
command.Parameters.AddWithValue(passwordHash);
|
||||
command.Parameters.AddWithValue(createdAtUtc);
|
||||
command.Parameters.AddWithValue(normalizedRoles);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
await reader.ReadAsync(cancellationToken);
|
||||
return ReadUser(reader);
|
||||
}
|
||||
|
||||
public async Task StoreRefreshTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
insert into auth_refresh_tokens (id, user_id, token_hash, created_at_utc, expires_at_utc, revoked_at_utc)
|
||||
values ($1, $2, $3, $4, $5, null)
|
||||
""");
|
||||
command.Parameters.AddWithValue(Guid.NewGuid());
|
||||
command.Parameters.AddWithValue(userId);
|
||||
command.Parameters.AddWithValue(tokenHash);
|
||||
command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue(expiresAtUtc);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RefreshTokenRecord?> FindRefreshTokenAsync(string tokenHash, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
select id, user_id, token_hash, created_at_utc, expires_at_utc, revoked_at_utc
|
||||
from auth_refresh_tokens
|
||||
where token_hash = $1
|
||||
""");
|
||||
command.Parameters.AddWithValue(tokenHash);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (!await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RefreshTokenRecord(
|
||||
reader.GetGuid(0),
|
||||
reader.GetGuid(1),
|
||||
reader.GetString(2),
|
||||
reader.GetFieldValue<DateTimeOffset>(3),
|
||||
reader.GetFieldValue<DateTimeOffset>(4),
|
||||
reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5));
|
||||
}
|
||||
|
||||
public async Task RevokeRefreshTokenAsync(Guid refreshTokenId, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
update auth_refresh_tokens
|
||||
set revoked_at_utc = $2
|
||||
where id = $1 and revoked_at_utc is null
|
||||
""");
|
||||
command.Parameters.AddWithValue(refreshTokenId);
|
||||
command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task RevokeAllRefreshTokensAsync(Guid userId, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
update auth_refresh_tokens
|
||||
set revoked_at_utc = $2
|
||||
where user_id = $1 and revoked_at_utc is null
|
||||
""");
|
||||
command.Parameters.AddWithValue(userId);
|
||||
command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task StorePasswordResetTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
insert into auth_password_reset_tokens (id, user_id, token_hash, created_at_utc, expires_at_utc, consumed_at_utc)
|
||||
values ($1, $2, $3, $4, $5, null)
|
||||
""");
|
||||
command.Parameters.AddWithValue(Guid.NewGuid());
|
||||
command.Parameters.AddWithValue(userId);
|
||||
command.Parameters.AddWithValue(tokenHash);
|
||||
command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue(expiresAtUtc);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PasswordResetTokenRecord?> FindPasswordResetTokenAsync(string tokenHash, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
select id, user_id, token_hash, created_at_utc, expires_at_utc, consumed_at_utc
|
||||
from auth_password_reset_tokens
|
||||
where token_hash = $1
|
||||
""");
|
||||
command.Parameters.AddWithValue(tokenHash);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (!await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PasswordResetTokenRecord(
|
||||
reader.GetGuid(0),
|
||||
reader.GetGuid(1),
|
||||
reader.GetString(2),
|
||||
reader.GetFieldValue<DateTimeOffset>(3),
|
||||
reader.GetFieldValue<DateTimeOffset>(4),
|
||||
reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5));
|
||||
}
|
||||
|
||||
public async Task ConsumePasswordResetTokenAsync(Guid passwordResetTokenId, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
update auth_password_reset_tokens
|
||||
set consumed_at_utc = $2
|
||||
where id = $1 and consumed_at_utc is null
|
||||
""");
|
||||
command.Parameters.AddWithValue(passwordResetTokenId);
|
||||
command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpdatePasswordHashAsync(Guid userId, string passwordHash, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = dataSource.CreateCommand("""
|
||||
update auth_users
|
||||
set password_hash = $2
|
||||
where id = $1
|
||||
""");
|
||||
command.Parameters.AddWithValue(userId);
|
||||
command.Parameters.AddWithValue(passwordHash);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static UserAccount ReadUser(NpgsqlDataReader reader) => new(
|
||||
reader.GetGuid(0),
|
||||
reader.GetString(1),
|
||||
reader.GetString(2),
|
||||
reader.GetFieldValue<DateTimeOffset>(3),
|
||||
reader.GetFieldValue<string[]>(4));
|
||||
}
|
||||
Reference in New Issue
Block a user