using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using PlotLine.Data; using PlotLine.Models; using PlotLine.ViewModels; namespace PlotLine.Services; public sealed class PasswordService : IPasswordService { private readonly PasswordHasher _passwordHasher = new(); public string HashPassword(string password) { return _passwordHasher.HashPassword(new AppUser(), password); } public bool VerifyPassword(string password, string passwordHash) { var result = _passwordHasher.VerifyHashedPassword(new AppUser(), passwordHash, password); return result is PasswordVerificationResult.Success or PasswordVerificationResult.SuccessRehashNeeded; } } public sealed class CurrentUserService(IHttpContextAccessor httpContextAccessor) : ICurrentUserService { private ClaimsPrincipal? User => httpContextAccessor.HttpContext?.User; public int? UserId { get { var value = User?.FindFirstValue(ClaimTypes.NameIdentifier); return int.TryParse(value, out var userId) ? userId : null; } } public string? Email => User?.FindFirstValue(ClaimTypes.Email); public string? DisplayName => User?.FindFirstValue(ClaimTypes.Name); public bool IsAuthenticated => User?.Identity?.IsAuthenticated == true; } public sealed class AuthService( IUserRepository users, IAuthenticationRepository authentication, IPasswordService passwords, IHttpContextAccessor httpContextAccessor, ILogger logger) : IAuthService { public async Task RegisterAsync(RegisterViewModel model) { var email = Clean(model.Email); var displayName = Clean(model.DisplayName); var existingUser = await users.GetByEmailAsync(email); if (existingUser is not null) { return RegistrationResult.Failed("An account already exists for that email address."); } var passwordHash = passwords.HashPassword(model.Password); var created = await users.CreateAsync(email, displayName, passwordHash, emailConfirmed: true); logger.LogInformation("User {UserID} registered with email confirmation temporarily enabled for Phase 4B.", created.UserID); return RegistrationResult.Succeeded(created.UserID); } public async Task LoginAsync(LoginViewModel model) { var email = Clean(model.Email); var user = await users.GetByEmailAsync(email); if (user is null) { await InsertLoginAuditAsync(null, email, false, "User not found"); return AuthenticationResult.Failed("The email address or password was not recognised."); } if (!passwords.VerifyPassword(model.Password, user.PasswordHash)) { await users.UpdateFailedLoginAsync(user.UserID, lockoutEndUtc: null); await InsertLoginAuditAsync(user.UserID, email, false, "Invalid password"); return AuthenticationResult.Failed("The email address or password was not recognised."); } var updatedUser = await users.UpdateLoginSuccessAsync(user.UserID) ?? user; await InsertLoginAuditAsync(user.UserID, email, true, "Login successful"); await SignInAsync(updatedUser, model.RememberMe); return AuthenticationResult.Succeeded(); } public async Task LogoutAsync() { var httpContext = httpContextAccessor.HttpContext; if (httpContext is not null) { await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } } public Task CompleteTwoFactorLoginAsync(TwoFactorViewModel model) => throw new NotImplementedException("Two-factor login will be implemented in a later phase."); public Task VerifyEmailAsync(Guid token) => throw new NotImplementedException("Email verification will be implemented in a later phase."); public Task ForgotPasswordAsync(ForgotPasswordViewModel model) => throw new NotImplementedException("Forgotten password handling will be implemented in a later phase."); public Task ResetPasswordAsync(ResetPasswordViewModel model) => throw new NotImplementedException("Password reset handling will be implemented in a later phase."); public Task ChangePasswordAsync(ChangePasswordViewModel model) => throw new NotImplementedException("Password changes will be implemented in a later phase."); public Task EnableTwoFactorAsync(int userId) => throw new NotImplementedException("Two-factor setup will be implemented in a later phase."); public Task DisableTwoFactorAsync(int userId) => throw new NotImplementedException("Two-factor setup will be implemented in a later phase."); public Task> GenerateRecoveryCodesAsync(int userId) => throw new NotImplementedException("Recovery codes will be implemented in a later phase."); private async Task SignInAsync(AppUser user, bool rememberMe) { var claims = new List { new(ClaimTypes.NameIdentifier, user.UserID.ToString()), new(ClaimTypes.Name, user.DisplayName), new(ClaimTypes.Email, user.Email) }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); var properties = new AuthenticationProperties { IsPersistent = rememberMe, AllowRefresh = true }; var httpContext = httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No active HTTP context is available."); await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, properties); } private async Task InsertLoginAuditAsync(int? userId, string emailAttempted, bool wasSuccessful, string reason) { var httpContext = httpContextAccessor.HttpContext; await authentication.InsertLoginAuditAsync(new UserLoginAudit { UserID = userId, EmailAttempted = emailAttempted, IpAddress = httpContext?.Connection.RemoteIpAddress?.ToString(), UserAgent = httpContext?.Request.Headers.UserAgent.ToString(), WasSuccessful = wasSuccessful, Reason = reason }); } private static string Clean(string value) => value.Trim(); }