using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Helpers; using SeaHaven.Services.Interfaces; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; namespace SeaHaven.Services.Implementation { public class AuthenticationService : IAuthenticationService { private readonly UserManager _userManager; private readonly JwtOptions _jwtOptions; private readonly IUserDataService _userDataService; private readonly IForgetPasswordDataService _forgetPasswordDataService; private readonly IEmailSender _emailSender; public AuthenticationService( UserManager userManager, IOptions jwtOptions, IUserDataService userDataService, IForgetPasswordDataService forgetPasswordDataService, IEmailSender emailSender) { _userManager = userManager; _jwtOptions = jwtOptions.Value; _userDataService = userDataService; _forgetPasswordDataService = forgetPasswordDataService; _emailSender = emailSender; } public async Task LoginAsync(string? username, string? password, CancellationToken cancellationToken) { var user = await _userManager.FindByNameAsync(username ?? ""); if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, password ?? "")) return await CreateSessionAsync(user, cancellationToken); return null; } public async Task CreateSessionAsync(ApplicationUser user, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(user); cancellationToken.ThrowIfCancellationRequested(); var userRoles = await _userManager.GetRolesAsync(user); var authClaims = new List { new Claim(ClaimTypes.Name, user.UserName ?? ""), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; foreach (var userRole in userRoles) { authClaims.Add(new Claim(ClaimTypes.Role, userRole)); } if (user.AccountId.HasValue) { authClaims.Add(new Claim( SeaHavenClaimTypes.AccountId, user.AccountId.Value.ToString())); } else if (userRoles.Contains("Admin")) { // Explicit signed org-wide elevation — never elevate via absence of account_id. authClaims.Add(new Claim( SeaHavenClaimTypes.OrgScope, SeaHavenClaimTypes.OrgScopeAll)); } var token = GetToken(authClaims); return new LoginResultDTO { Token = new JwtSecurityTokenHandler().WriteToken(token), Expiration = token.ValidTo, Email = user.Email, UserRole = userRoles.FirstOrDefault(), PhoneNumber = user.PhoneNumber, Fullname = $"{user.FirstName} {user.LastName}".Trim(), Id = user.Id }; } public async Task ChangePasswordAsync(string userId, string? currentPassword, string? newPassword, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var user = await _userManager.FindByIdAsync(userId); if (user == null || user.IsDeleted == true) return ChangePasswordResult(ChangePasswordStatus.CurrentPasswordIncorrect); // The current password is verified before the new one is evaluated, so a // caller without it learns nothing about the policy outcome. if (!await _userManager.CheckPasswordAsync(user, currentPassword ?? "")) return ChangePasswordResult(ChangePasswordStatus.CurrentPasswordIncorrect); var result = await _userManager.ChangePasswordAsync(user, currentPassword ?? "", newPassword ?? ""); if (result.Succeeded) return ChangePasswordResult(ChangePasswordStatus.Succeeded); return ChangePasswordResult(IdentityPasswordPolicy.IsPolicyRejection(result) ? ChangePasswordStatus.PasswordRejected : ChangePasswordStatus.Failed); } private static ChangePasswordResultDTO ChangePasswordResult(ChangePasswordStatus status) => new() { Status = status }; public async Task UpdateProfileAsync(string userId, UpdateProfileRequestDTO dto, CancellationToken cancellationToken) { var exists = await _userDataService.UpdateProfileAsync( userId, dto.Name, dto.Email, dto.Contact, cancellationToken); if (!exists) return null; var updated = await _userDataService.GetProfileAsync(userId, cancellationToken); if (updated == null) return null; return new UserProfileDTO { FirstName = updated.FirstName, Email = updated.Email, Contact = updated.Contact }; } public async Task ForgetPasswordAsync(string email, CancellationToken cancellationToken) { var user = await _userDataService.GetByEmailNormalizedAsync(email, cancellationToken); if (user == null) return false; var code = GenerateRandomNo(); await _forgetPasswordDataService.ReplaceCodeAsync(user.Email ?? "", user.Id, code, cancellationToken); var body = $"Your Password Reset Code is: " + code; await _emailSender.SendEmailAsync(user.Email ?? email, "Forget Password Request.", body); return true; } public async Task VerifyCodeAsync(string code, CancellationToken cancellationToken) { return await _forgetPasswordDataService.CodeExistsAsync(code, cancellationToken); } public async Task ResetPasswordAsync(string email, string? code, string? password, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(password)) return false; var matched = await _forgetPasswordDataService.ExistsByEmailAndCodeAsync(email, code, cancellationToken); if (!matched) return false; var record = await _forgetPasswordDataService.GetByEmailAsync(email, cancellationToken); if (record == null) return false; var user = await _userManager.FindByIdAsync(record.UserId); if (user == null) return false; var token = await _userManager.GeneratePasswordResetTokenAsync(user); var result = await _userManager.ResetPasswordAsync(user, token, password); if (!result.Succeeded) return false; await _forgetPasswordDataService.RemoveByEmailAsync(email, cancellationToken); return true; } private JwtSecurityToken GetToken(List authClaims) { var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.Secret)); var token = new JwtSecurityToken( issuer: _jwtOptions.ValidIssuer, audience: _jwtOptions.ValidAudience, expires: DateTime.Now.AddDays(10), claims: authClaims, signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256) ); return token; } private static string GenerateRandomNo() { return RandomNumberGenerator.GetInt32(1_000_000).ToString("D6"); } } }