Merge pull request 'Implement an API to Generate MPIN' (#84) from Ashutosh_Task#471_Create_MPIN into Issue_Jun_1W_2
Reviewed-on: #84
This commit is contained in:
commit
863a154ec6
8
Marco.Pms.Model/Dtos/Authentication/GenerateMPINDto.cs
Normal file
8
Marco.Pms.Model/Dtos/Authentication/GenerateMPINDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Marco.Pms.Model.Dtos.Authentication
|
||||||
|
{
|
||||||
|
public class GenerateMPINDto
|
||||||
|
{
|
||||||
|
public Guid EmployeeId { get; set; }
|
||||||
|
public string? MPIN { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using Marco.Pms.DataAccess.Data;
|
using Marco.Pms.DataAccess.Data;
|
||||||
using Marco.Pms.Model.Authentication;
|
using Marco.Pms.Model.Authentication;
|
||||||
using Marco.Pms.Model.Dtos.Authentication;
|
using Marco.Pms.Model.Dtos.Authentication;
|
||||||
@ -12,7 +14,6 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NuGet.Common;
|
|
||||||
|
|
||||||
namespace MarcoBMS.Services.Controllers
|
namespace MarcoBMS.Services.Controllers
|
||||||
{
|
{
|
||||||
@ -28,9 +29,10 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
private readonly IEmailSender _emailSender;
|
private readonly IEmailSender _emailSender;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly EmployeeHelper _employeeHelper;
|
private readonly EmployeeHelper _employeeHelper;
|
||||||
|
private readonly ILoggingService _logger;
|
||||||
//string tenentId = "1";
|
//string tenentId = "1";
|
||||||
public AuthController(UserManager<ApplicationUser> userManager, ApplicationDbContext context, JwtSettings jwtSettings, RefreshTokenService refreshTokenService,
|
public AuthController(UserManager<ApplicationUser> 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;
|
_userManager = userManager;
|
||||||
_jwtSettings = jwtSettings;
|
_jwtSettings = jwtSettings;
|
||||||
@ -39,7 +41,8 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_employeeHelper = employeeHelper;
|
_employeeHelper = employeeHelper;
|
||||||
_context = context;
|
_context = context;
|
||||||
_userHelper= userHelper;
|
_userHelper = userHelper;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
@ -133,7 +136,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
|
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
|
||||||
|
|
||||||
// Generate MPIN Token (custom short-term token)
|
// 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
|
// Combine all tokens in response
|
||||||
var responseData = new
|
var responseData = new
|
||||||
@ -147,7 +150,6 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return Ok(ApiResponse<object>.SuccessResponse(responseData, "User logged in successfully.", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(responseData, "User logged in successfully.", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
|
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
|
||||||
{
|
{
|
||||||
@ -318,50 +320,148 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Password reset link sent.", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Password reset link sent.", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpPost("change-password")]
|
[HttpPost("change-password")]
|
||||||
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordDto changePassword )
|
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordDto changePassword)
|
||||||
{
|
{
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Get the currently logged-in user
|
||||||
var loggedUser = await _userHelper.GetCurrentUserAsync();
|
var loggedUser = await _userHelper.GetCurrentUserAsync();
|
||||||
if (changePassword.Email == null)
|
|
||||||
|
// Validate email
|
||||||
|
if (string.IsNullOrWhiteSpace(changePassword.Email))
|
||||||
{
|
{
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Email is missing", "Email is missing",400));
|
_logger.LogWarning("Change password attempt failed - Email is missing");
|
||||||
}
|
return BadRequest(ApiResponse<object>.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<object>.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<object>.SuccessResponse(result.Succeeded, "Password Changed successfully.", 200));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return BadRequest(ApiResponse<object>.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<object>.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<object>.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<object>.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<object>.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<object>.ErrorResponse("An unexpected error occurred.", exp.Message, 500));
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", exp.Message, 500));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost("generate-mpin")]
|
||||||
|
public async Task<IActionResult> 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<object>.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<object>.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<object>.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<object>.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -98,44 +98,36 @@ namespace MarcoBMS.Services.Service
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task<string> CreateMPINToken(string userId, string tenantId, JwtSettings jwtSettings)
|
public string CreateMPINToken(string userId, string tenantId, JwtSettings jwtSettings)
|
||||||
{
|
{
|
||||||
try
|
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(ClaimTypes.NameIdentifier, userId),
|
||||||
new Claim("TenantId", tenantId),
|
new Claim("TenantId", tenantId),
|
||||||
new Claim("token_type", "mpin")
|
new Claim("token_type", "mpin")
|
||||||
};
|
};
|
||||||
|
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key));
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key));
|
||||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
|
||||||
|
|
||||||
var tokenDescriptor = new SecurityTokenDescriptor
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
{
|
{
|
||||||
Subject = new ClaimsIdentity(claims),
|
Subject = new ClaimsIdentity(claims),
|
||||||
Issuer = jwtSettings.Issuer,
|
Issuer = jwtSettings.Issuer,
|
||||||
Audience = jwtSettings.Audience,
|
Audience = jwtSettings.Audience,
|
||||||
SigningCredentials = creds
|
SigningCredentials = creds
|
||||||
// No 'Expires' means the token won't expire
|
// No 'Expires' means the token won't expire
|
||||||
};
|
};
|
||||||
|
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
var MPINToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
|
var MPINToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
|
||||||
|
return MPINToken;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
existingMPIN.MPINToken = MPINToken;
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return MPINToken;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -201,5 +193,35 @@ namespace MarcoBMS.Services.Service
|
|||||||
return jwtToken?.ValidTo;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user