shoc-backend/SeaHaven.Services/Implementation/AuthenticationService.cs

194 lines
8.2 KiB
C#

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<ApplicationUser> _userManager;
private readonly JwtOptions _jwtOptions;
private readonly IUserDataService _userDataService;
private readonly IForgetPasswordDataService _forgetPasswordDataService;
private readonly IEmailSender _emailSender;
public AuthenticationService(
UserManager<ApplicationUser> userManager,
IOptions<JwtOptions> jwtOptions,
IUserDataService userDataService,
IForgetPasswordDataService forgetPasswordDataService,
IEmailSender emailSender)
{
_userManager = userManager;
_jwtOptions = jwtOptions.Value;
_userDataService = userDataService;
_forgetPasswordDataService = forgetPasswordDataService;
_emailSender = emailSender;
}
public async Task<LoginResultDTO?> 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<LoginResultDTO> CreateSessionAsync(ApplicationUser user, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(user);
cancellationToken.ThrowIfCancellationRequested();
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
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<ChangePasswordResultDTO> 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<UserProfileDTO?> 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<bool> 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<bool> VerifyCodeAsync(string code, CancellationToken cancellationToken)
{
return await _forgetPasswordDataService.CodeExistsAsync(code, cancellationToken);
}
public async Task<bool> 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<Claim> 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");
}
}
}