diff --git a/Marco.Pms.Model/Dtos/Authentication/GenerateMPINDto.cs b/Marco.Pms.Model/Dtos/Authentication/GenerateMPINDto.cs new file mode 100644 index 0000000..e762ccb --- /dev/null +++ b/Marco.Pms.Model/Dtos/Authentication/GenerateMPINDto.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Dtos.Authentication +{ + public class GenerateMPINDto + { + public Guid EmployeeId { get; set; } + public string? MPIN { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 98a49c3..e16e640 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -1,4 +1,6 @@ using System.Net; +using System.Security.Cryptography; +using System.Text; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Dtos.Authentication; @@ -12,7 +14,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using NuGet.Common; namespace MarcoBMS.Services.Controllers { @@ -28,9 +29,10 @@ namespace MarcoBMS.Services.Controllers private readonly IEmailSender _emailSender; private readonly IConfiguration _configuration; private readonly EmployeeHelper _employeeHelper; + private readonly ILoggingService _logger; //string tenentId = "1"; public AuthController(UserManager userManager, ApplicationDbContext context, JwtSettings jwtSettings, RefreshTokenService refreshTokenService, - IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper, UserHelper userHelper) + IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper, UserHelper userHelper, ILoggingService logger) { _userManager = userManager; _jwtSettings = jwtSettings; @@ -39,7 +41,8 @@ namespace MarcoBMS.Services.Controllers _configuration = configuration; _employeeHelper = employeeHelper; _context = context; - _userHelper= userHelper; + _userHelper = userHelper; + _logger = logger; } [HttpPost("login")] @@ -133,7 +136,7 @@ namespace MarcoBMS.Services.Controllers var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); // Generate MPIN Token (custom short-term token) - var mpinToken = await _refreshTokenService.CreateMPINToken(user.Id, emp.TenantId.ToString(), _jwtSettings); + var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id) && p.TenantId == emp.TenantId); // Combine all tokens in response var responseData = new @@ -147,7 +150,6 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(responseData, "User logged in successfully.", 200)); } - [HttpPost("logout")] public async Task Logout([FromBody] LogoutDto logoutDto) { @@ -318,50 +320,148 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(new { }, "Password reset link sent.", 200)); } - [Authorize] [HttpPost("change-password")] - public async Task ChangePassword([FromBody] ChangePasswordDto changePassword ) + public async Task ChangePassword([FromBody] ChangePasswordDto changePassword) { - try { + // Get the currently logged-in user var loggedUser = await _userHelper.GetCurrentUserAsync(); - if (changePassword.Email == null) + + // Validate email + if (string.IsNullOrWhiteSpace(changePassword.Email)) { - return BadRequest(ApiResponse.ErrorResponse("Email is missing", "Email is missing",400)); - } - ApplicationUser? requestedUser = await _userManager.FindByEmailAsync(changePassword.Email); - bool IsOldPassword = await _userManager.CheckPasswordAsync(requestedUser ?? new ApplicationUser(), changePassword.OldPassword ?? string.Empty); - if (requestedUser != null && loggedUser?.Email == requestedUser?.Email && IsOldPassword) - { - var token = await _userManager.GeneratePasswordResetTokenAsync(requestedUser ?? new ApplicationUser()); - - var result = await _userManager.ResetPasswordAsync(requestedUser ?? new ApplicationUser(), token, changePassword.NewPassword ?? string.Empty); - - - if (!result.Succeeded) - { - var errors = result.Errors.Select(e => e.Description).ToList(); - return BadRequest(ApiResponse.ErrorResponse("Failed to Change password", errors, 400)); - } - - - Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(loggedUser?.Id ?? string.Empty); - await _emailSender.SendResetPasswordSuccessEmail(loggedUser?.Email ?? string.Empty, emp.FirstName + " " + emp.LastName); - - return Ok(ApiResponse.SuccessResponse(result.Succeeded, "Password Changed successfully.", 200)); - + _logger.LogWarning("Change password attempt failed - Email is missing"); + return BadRequest(ApiResponse.ErrorResponse("Email is missing", "Email is missing", 400)); } - return BadRequest(ApiResponse.ErrorResponse("Incorrect Password and Email", "Invalid request.", 400)); + // Find the user by email + var requestedUser = await _userManager.FindByEmailAsync(changePassword.Email); + if (requestedUser == null) + { + _logger.LogWarning("Change password attempt failed - Email not found: {Email}", changePassword.Email); + return BadRequest(ApiResponse.ErrorResponse("Invalid email", "User not found.", 400)); + } + // Validate the old password + bool isOldPasswordCorrect = await _userManager.CheckPasswordAsync(requestedUser, changePassword.OldPassword ?? string.Empty); + + // Ensure user identity and old password match + if (loggedUser?.Email != requestedUser.Email || !isOldPasswordCorrect) + { + _logger.LogWarning("Change password denied - User {Email} provided incorrect credentials", changePassword.Email); + return BadRequest(ApiResponse.ErrorResponse("Incorrect credentials", "Invalid request.", 400)); + } + + // Generate reset token and change password + var resetToken = await _userManager.GeneratePasswordResetTokenAsync(requestedUser); + var result = await _userManager.ResetPasswordAsync(requestedUser, resetToken, changePassword.NewPassword ?? string.Empty); + + if (!result.Succeeded) + { + var errors = result.Errors.Select(e => e.Description).ToList(); + _logger.LogError("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Failed to change password", errors, 400)); + } + + // Send confirmation email + var emp = await _employeeHelper.GetEmployeeByApplicationUserID(requestedUser.Id); + await _emailSender.SendResetPasswordSuccessEmail(requestedUser.Email ?? string.Empty, $"{emp.FirstName} {emp.LastName}"); + + _logger.LogInfo("Password changed successfully for user {Email}", requestedUser.Email ?? string.Empty); + return Ok(ApiResponse.SuccessResponse(true, "Password changed successfully.", 200)); } - catch(Exception exp) + catch (Exception exp) { + _logger.LogError("An unexpected error occurred while changing password : {Error}", exp.Message); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred.", exp.Message, 500)); } + } + [Authorize] + [HttpPost("generate-mpin")] + public async Task GenerateMPIN([FromBody] GenerateMPINDto generateMPINDto) + { + Guid tenantId = _userHelper.GetTenantId(); + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Get the employee for whom MPIN is being generated + var requestEmployee = await _context.Employees + .Include(e => e.ApplicationUser) + .FirstOrDefaultAsync(e => e.Id == generateMPINDto.EmployeeId && e.TenantId == tenantId); + + // Validate employee and MPIN input + if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN)) + { + _logger.LogError("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id); + return BadRequest(ApiResponse.ErrorResponse("Provided invalid information", "Provided invalid information", 400)); + } + + // Ensure the logged-in user is only generating their own MPIN + if (requestEmployee.Id != loggedInEmployee.Id) + { + _logger.LogWarning("Employee {EmployeeId} tried to set MPIN for a different employee", loggedInEmployee.Id); + return BadRequest(ApiResponse.ErrorResponse("You can't create MPIN for another employee", "Unauthorized MPIN creation", 400)); + } + + // Generate hash and token + string mpinHash = ComputeSha256Hash(generateMPINDto.MPIN); + string mpinToken = _refreshTokenService.CreateMPINToken( + requestEmployee.ApplicationUserId, + requestEmployee.TenantId.ToString(), + _jwtSettings + ); + + // Prepare MPIN entity + Guid userId = Guid.Parse(requestEmployee.ApplicationUserId ?? string.Empty); + var existingMPIN = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == userId && p.TenantId == tenantId); + + if (existingMPIN == null) + { + // Add new MPIN record + var mPINDetails = new MPINDetails + { + UserId = userId, + MPIN = mpinHash, + MPINToken = mpinToken, + TimeStamp = DateTime.UtcNow, + TenantId = tenantId + }; + + _context.MPINDetails.Add(mPINDetails); + await _context.SaveChangesAsync(); + + _logger.LogInfo("MPIN generated successfully for employee {EmployeeId}", requestEmployee.Id); + return Ok(ApiResponse.SuccessResponse(mpinToken, "MPIN generated successfully", 200)); + } + else + { + // Update existing MPIN record + existingMPIN.MPIN = mpinHash; + existingMPIN.MPINToken = mpinToken; + existingMPIN.TimeStamp = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + _logger.LogInfo("MPIN updated successfully for employee {EmployeeId}", requestEmployee.Id); + return Ok(ApiResponse.SuccessResponse(mpinToken, "MPIN updated successfully", 200)); + } + } + private static string ComputeSha256Hash(string rawData) + { + using (SHA256 sha256 = SHA256.Create()) + { + // Convert the input string to bytes and compute the hash + byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawData)); + + // Convert byte array to a readable hex string + StringBuilder builder = new StringBuilder(); + foreach (var b in bytes) + builder.Append(b.ToString("x2")); + + return builder.ToString(); + } } } diff --git a/Marco.Pms.Services/Service/RefreshTokenService.cs b/Marco.Pms.Services/Service/RefreshTokenService.cs index ea05f76..018de68 100644 --- a/Marco.Pms.Services/Service/RefreshTokenService.cs +++ b/Marco.Pms.Services/Service/RefreshTokenService.cs @@ -98,44 +98,36 @@ namespace MarcoBMS.Services.Service throw; } } - public async Task CreateMPINToken(string userId, string tenantId, JwtSettings jwtSettings) + public string CreateMPINToken(string userId, string tenantId, JwtSettings jwtSettings) { try { - var existingMPIN = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(userId) && p.TenantId == Guid.Parse(tenantId)); - if (existingMPIN != null) + + var claims = new[] { - var claims = new[] - { new Claim(ClaimTypes.NameIdentifier, userId), new Claim("TenantId", tenantId), new Claim("token_type", "mpin") }; - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); - var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(claims), - Issuer = jwtSettings.Issuer, - Audience = jwtSettings.Audience, - SigningCredentials = creds - // No 'Expires' means the token won't expire - }; + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Issuer = jwtSettings.Issuer, + Audience = jwtSettings.Audience, + SigningCredentials = creds + // No 'Expires' means the token won't expire + }; - var tokenHandler = new JwtSecurityTokenHandler(); - var MPINToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); + var tokenHandler = new JwtSecurityTokenHandler(); + var MPINToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); + return MPINToken; - existingMPIN.MPINToken = MPINToken; - await _context.SaveChangesAsync(); - return MPINToken; - } - return null; - - } catch (Exception ex) @@ -201,5 +193,35 @@ namespace MarcoBMS.Services.Service return jwtToken?.ValidTo; } + public ClaimsPrincipal ValidateToken(string token, JwtSettings jwtSettings) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = System.Text.Encoding.ASCII.GetBytes(jwtSettings.Key); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidIssuer = jwtSettings.Issuer, + ValidateAudience = true, + ValidAudience = jwtSettings.Audience, + ValidateLifetime = false, // Disable lifetime validation (ignores expiration) + ClockSkew = TimeSpan.Zero // Optional: Remove time skew buffer + }; + + try + { + var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); + return principal; + } + catch (Exception ex) + { + // Token is invalid + Console.WriteLine($"Token validation failed: {ex.Message}"); + return null; + } + } + } }