Refactor runtime bootstrap and ship control flows

This commit is contained in:
2026-04-03 01:12:26 -04:00
parent 0bb72bee35
commit 706e1cda8f
129 changed files with 9588 additions and 3548 deletions

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