217 lines
9.6 KiB
C#
217 lines
9.6 KiB
C#
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<IReadOnlyList<UserAccount>> ListUsersAsync(CancellationToken cancellationToken)
|
|
{
|
|
await using var command = dataSource.CreateCommand("""
|
|
select id, email, password_hash, created_at_utc, roles
|
|
from auth_users
|
|
order by email asc
|
|
""");
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
var users = new List<UserAccount>();
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
users.Add(ReadUser(reader));
|
|
}
|
|
|
|
return users;
|
|
}
|
|
|
|
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));
|
|
}
|