mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 14:13:12 +00:00
* refactor(api): enforce service and data-service boundaries * refactor(api): complete feature service boundaries * refactor(identity): enforce service and data boundaries * refactor(vendors): enforce service and data boundaries * refactor(workorders): enforce service and data boundaries * refactor(backend): enforce architecture and optimize dispatch * style(backend): format changed architecture files * fix(architecture): address backend review follow-ups * fix(backend): sanitize exception disclosure in changed API endpoints Replace raw exception-message disclosure (ex.Message) returned to API callers with a stable sanitized public message plus correlated structured internal logging, across the endpoints changed in this PR. - Add SanitizedErrors helper: logs the original exception at Error with a generated correlation id and returns a stable public message referencing it so support can trace without exposing internals. - Inject ILogger<T> into the 14 changed controllers and route every ex.Message/dbex.Message disclosure through the helper, preserving status codes, response shapes, and business data (e.g. OpenWorkOrders). - Leave FluentValidation (vex.Errors) and existing fixed-message catches untouched; out-of-scope controllers (Account/Contact/Employee/Asset/ PMSchedule) are unchanged. - Add focused tests proving internal exception text is not returned and that Error logging carrying the original exception is invoked. * fix(architecture): abstract job run state access * style: format board update service * test: use collection assertion idiom
160 lines
6.5 KiB
C#
160 lines
6.5 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.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 ?? ""))
|
|
{
|
|
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));
|
|
}
|
|
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,
|
|
Id = user.Id
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async Task<bool> ChangePasswordAsync(string userId, string? currentPassword, string? confirmPassword, CancellationToken cancellationToken)
|
|
{
|
|
var user = await _userManager.FindByIdAsync(userId);
|
|
if (user == null)
|
|
return false;
|
|
|
|
var result = await _userManager.ChangePasswordAsync(user, currentPassword ?? "", confirmPassword ?? "");
|
|
return result.Succeeded;
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|