Files
space-game/apps/backend/Auth/Simulation/AuthService.cs

121 lines
5.4 KiB
C#

namespace SpaceGame.Api.Auth.Simulation;
public sealed class AuthService(
IAuthRepository authRepository,
LocalPasswordHasher passwordHasher,
ITokenService tokenService,
RefreshTokenFactory refreshTokenFactory,
IPasswordResetDelivery passwordResetDelivery)
{
public async Task<AuthSessionResponse> RegisterAsync(RegisterRequest request, CancellationToken cancellationToken)
{
var email = NormalizeEmail(request.Email);
ValidatePassword(request.Password);
if (await authRepository.FindUserByEmailAsync(email, cancellationToken) is not null)
{
throw new InvalidOperationException("An account already exists for that email.");
}
var user = await authRepository.CreateUserAsync(email, passwordHasher.HashPassword(request.Password), [], cancellationToken);
return await CreateSessionAsync(user, cancellationToken);
}
public async Task<AuthSessionResponse> LoginAsync(LoginRequest request, CancellationToken cancellationToken)
{
var email = NormalizeEmail(request.Email);
var user = await authRepository.FindUserByEmailAsync(email, cancellationToken)
?? throw new InvalidOperationException("Invalid email or password.");
if (!passwordHasher.VerifyPassword(request.Password, user.PasswordHash))
{
throw new InvalidOperationException("Invalid email or password.");
}
return await CreateSessionAsync(user, cancellationToken);
}
public async Task<AuthSessionResponse> RefreshAsync(RefreshTokenRequest request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.RefreshToken))
{
throw new InvalidOperationException("Refresh token is required.");
}
var tokenHash = refreshTokenFactory.HashToken(request.RefreshToken);
var record = await authRepository.FindRefreshTokenAsync(tokenHash, cancellationToken)
?? throw new InvalidOperationException("Refresh token is invalid.");
if (record.RevokedAtUtc is not null || record.ExpiresAtUtc <= DateTimeOffset.UtcNow)
{
throw new InvalidOperationException("Refresh token is expired.");
}
var user = await authRepository.FindUserByIdAsync(record.UserId, cancellationToken)
?? throw new InvalidOperationException("User account was not found.");
await authRepository.RevokeRefreshTokenAsync(record.Id, cancellationToken);
return await CreateSessionAsync(user, cancellationToken);
}
public async Task<ForgotPasswordResponse> ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken cancellationToken)
{
var email = NormalizeEmail(request.Email);
var user = await authRepository.FindUserByEmailAsync(email, cancellationToken);
if (user is null)
{
return new ForgotPasswordResponse(true);
}
var resetToken = refreshTokenFactory.CreateToken();
var resetTokenHash = refreshTokenFactory.HashToken(resetToken);
var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(30);
await authRepository.StorePasswordResetTokenAsync(user.Id, resetTokenHash, expiresAtUtc, cancellationToken);
return await passwordResetDelivery.DeliverAsync(user, resetToken, cancellationToken);
}
public async Task ResetPasswordAsync(ResetPasswordRequest request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Token))
{
throw new InvalidOperationException("Reset token is required.");
}
ValidatePassword(request.NewPassword);
var tokenHash = refreshTokenFactory.HashToken(request.Token);
var record = await authRepository.FindPasswordResetTokenAsync(tokenHash, cancellationToken)
?? throw new InvalidOperationException("Reset token is invalid.");
if (record.ConsumedAtUtc is not null || record.ExpiresAtUtc <= DateTimeOffset.UtcNow)
{
throw new InvalidOperationException("Reset token is expired.");
}
await authRepository.UpdatePasswordHashAsync(record.UserId, passwordHasher.HashPassword(request.NewPassword), cancellationToken);
await authRepository.ConsumePasswordResetTokenAsync(record.Id, cancellationToken);
await authRepository.RevokeAllRefreshTokensAsync(record.UserId, cancellationToken);
}
private async Task<AuthSessionResponse> CreateSessionAsync(UserAccount user, CancellationToken cancellationToken)
{
var (accessToken, accessExpiresAtUtc) = tokenService.CreateAccessToken(user);
var (refreshToken, refreshTokenHash, refreshExpiresAtUtc) = tokenService.CreateRefreshToken();
await authRepository.StoreRefreshTokenAsync(user.Id, refreshTokenHash, refreshExpiresAtUtc, cancellationToken);
return new AuthSessionResponse(user.Id, user.Email, user.Roles, accessToken, accessExpiresAtUtc, refreshToken, refreshExpiresAtUtc);
}
private static string NormalizeEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
throw new InvalidOperationException("Email is required.");
}
return email.Trim().ToLowerInvariant();
}
private static void ValidatePassword(string password)
{
if (string.IsNullOrWhiteSpace(password) || password.Length < 8)
{
throw new InvalidOperationException("Password must be at least 8 characters.");
}
}
}