namespace SpaceGame.Api.Auth.Simulation; public sealed class AuthService( IAuthRepository authRepository, LocalPasswordHasher passwordHasher, ITokenService tokenService, RefreshTokenFactory refreshTokenFactory, IPasswordResetDelivery passwordResetDelivery) { public async Task 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 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 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 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 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."); } } }