From 775c17531bb7d4187a74d20fbbe4e5bd34d1715f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 7 Jun 2025 13:20:12 +0530 Subject: [PATCH] Implement API to log in through MPIN authentication. --- .../Dtos/Authentication/VerifyMPINDto.cs | 9 + .../Controllers/AuthController.cs | 341 ++++++++++++++---- 2 files changed, 281 insertions(+), 69 deletions(-) create mode 100644 Marco.Pms.Model/Dtos/Authentication/VerifyMPINDto.cs diff --git a/Marco.Pms.Model/Dtos/Authentication/VerifyMPINDto.cs b/Marco.Pms.Model/Dtos/Authentication/VerifyMPINDto.cs new file mode 100644 index 0000000..ebb200d --- /dev/null +++ b/Marco.Pms.Model/Dtos/Authentication/VerifyMPINDto.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Dtos.Authentication +{ + public class VerifyMPINDto + { + public Guid EmployeeId { get; set; } + public string? MPIN { get; set; } + public string? MPINToken { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 34b3f1a..510743e 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Security.Claims; using System.Security.Cryptography; using System.Text; using Marco.Pms.DataAccess.Data; @@ -48,34 +49,70 @@ namespace MarcoBMS.Services.Controllers [HttpPost("login")] public async Task Login([FromBody] LoginDto loginDto) { - var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username); - - if (user != null) + try { + // Find user by email or phone number + var user = await _context.ApplicationUsers + .FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username); + + if (user == null) + { + _logger.LogWarning("Login failed: User not found for input {Username}", loginDto.Username ?? string.Empty); + return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401)); + } + + // Check if the user is active if (!user.IsActive) { - return BadRequest(ApiResponse.ErrorResponse("User is In Active", "User is In Active", 400)); + _logger.LogWarning("Login failed: Inactive user attempted login - UserId: {UserId}", user.Id); + return BadRequest(ApiResponse.ErrorResponse("User is inactive", "User is inactive", 400)); } + + // Ensure the user's email is confirmed if (!user.EmailConfirmed) { - return BadRequest(ApiResponse.ErrorResponse("Your email is not verified, Please verify your email", "Your email is not verified, Please verify your email", 400)); + _logger.LogWarning("Login failed: Email not confirmed for UserId: {UserId}", user.Id); + return BadRequest(ApiResponse.ErrorResponse("Email not verified", "Your email is not verified, please verify your email", 400)); } - if (await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty)) + + // Validate the password + if (!await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty)) { - Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); - //var refreshToken = GenerateRefreshToken(); - if (user.UserName == null) return NotFound(ApiResponse.ErrorResponse("UserName Not found", "UserName Not found", 404)); ; - - var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings); - - var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); - - return Ok(ApiResponse.SuccessResponse(new { token = token, refreshToken = refreshToken }, "User logged in successfully.", 200)); + _logger.LogWarning("Login failed: Incorrect password for UserId: {UserId}", user.Id); + return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401)); } - } + // Retrieve employee details + var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); + if (emp == null) + { + _logger.LogWarning("Login failed: No employee record found for UserId: {UserId}", user.Id); + return NotFound(ApiResponse.ErrorResponse("Employee record not found", "Employee not found", 404)); + } - return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401)); + // Ensure UserName exists for JWT + if (string.IsNullOrWhiteSpace(user.UserName)) + { + _logger.LogWarning("Login failed: Username not found for UserId: {UserId}", user.Id); + return NotFound(ApiResponse.ErrorResponse("Username not found", "Username not found", 404)); + } + + // Generate tokens + var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings); + var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); + + _logger.LogInfo("User login successful - UserId: {UserId}", user.Id); + return Ok(ApiResponse.SuccessResponse(new + { + token, + refreshToken + }, "User logged in successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("Unexpected error during login : {Error}", ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); + } } [HttpPost("login-mobile")] @@ -150,111 +187,272 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(responseData, "User logged in successfully.", 200)); } + [HttpPost("login-mpin")] + public async Task VerifyMPIN([FromBody] VerifyMPINDto verifyMPIN) + { + try + { + // Validate the MPIN token and extract claims + var claimsPrincipal = _refreshTokenService.ValidateToken(verifyMPIN.MPINToken, _jwtSettings); + if (claimsPrincipal?.Identity == null || !claimsPrincipal.Identity.IsAuthenticated) + { + _logger.LogWarning("Invalid or unauthenticated MPIN token"); + return Unauthorized(ApiResponse.ErrorResponse("Invalid MPIN token", "Unauthorized", 401)); + } + + string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value; + string? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value; + string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + // Validate essential claims + if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenTenantId) || string.IsNullOrWhiteSpace(tokenUserId)) + { + _logger.LogWarning("MPIN token claims are incomplete"); + return Unauthorized(ApiResponse.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401)); + } + + Guid tenantId = Guid.Parse(tokenTenantId); + + // Fetch employee by ID and tenant + var requestEmployee = await _context.Employees + .Include(e => e.ApplicationUser) + .FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.TenantId == tenantId && e.ApplicationUserId == tokenUserId && e.IsActive); + + if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId)) + { + _logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId); + return BadRequest(ApiResponse.ErrorResponse("Invalid request", "Provided invalid employee information", 400)); + } + + // Validate that the token belongs to the same employee making the request + if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin") + { + _logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id); + return Unauthorized(ApiResponse.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401)); + } + + // Ensure MPIN input is valid + if (string.IsNullOrWhiteSpace(verifyMPIN.MPIN)) + { + _logger.LogWarning("MPIN not provided for EmployeeId: {EmployeeId}", requestEmployee.Id); + return BadRequest(ApiResponse.ErrorResponse("Invalid request", "MPIN not provided", 400)); + } + + // Retrieve MPIN details + var mpinDetails = await _context.MPINDetails + .FirstOrDefaultAsync(p => p.UserId == Guid.Parse(requestEmployee.ApplicationUserId) && p.TenantId == tenantId); + + if (mpinDetails == null) + { + _logger.LogWarning("MPIN not set for EmployeeId: {EmployeeId}", requestEmployee.Id); + return BadRequest(ApiResponse.ErrorResponse("MPIN not set", "You have not set an MPIN", 400)); + } + + // Compare hashed MPIN + var providedMPINHash = ComputeSha256Hash(verifyMPIN.MPIN); + if (providedMPINHash != mpinDetails.MPIN) + { + _logger.LogWarning("MPIN mismatch for EmployeeId: {EmployeeId}", requestEmployee.Id); + return Unauthorized(ApiResponse.ErrorResponse("MPIN mismatch", "MPIN did not match", 401)); + } + + // Generate new tokens + var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, _jwtSettings); + var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), _jwtSettings); + + _logger.LogInfo("MPIN verification successful - EmployeeId: {EmployeeId}", requestEmployee.Id); + + return Ok(ApiResponse.SuccessResponse(new + { + token = jwtToken, + refreshToken + }, "User logged in successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("Unexpected error occurred while verifying MPIN : {Error}", ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); + } + } + [HttpPost("logout")] public async Task Logout([FromBody] LogoutDto logoutDto) { - if (string.IsNullOrEmpty(logoutDto.RefreshToken)) + if (string.IsNullOrWhiteSpace(logoutDto.RefreshToken)) { + _logger.LogWarning("Logout failed: Refresh token is missing"); return BadRequest(ApiResponse.ErrorResponse("Refresh token is required", "Refresh token is required", 400)); - } try { // Revoke the refresh token bool isRevoked = await _refreshTokenService.RevokeRefreshTokenAsync(logoutDto.RefreshToken); - if (!isRevoked) + { + _logger.LogWarning("Logout failed: Invalid or expired refresh token"); return Unauthorized(ApiResponse.ErrorResponse("Invalid or expired refresh token", "Invalid or expired refresh token", 401)); + } - - // Optional: Blacklist the access token (JWT) + // Optional: Blacklist the JWT access token string jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); - if (!string.IsNullOrEmpty(jwtToken)) + if (!string.IsNullOrWhiteSpace(jwtToken)) { await _refreshTokenService.BlacklistJwtTokenAsync(jwtToken); + _logger.LogInfo("JWT access token blacklisted successfully"); } + + _logger.LogInfo("User logged out successfully"); return Ok(ApiResponse.SuccessResponse(new { }, "Logged out successfully", 200)); } catch (Exception ex) { - // _logger.LogError(ex, "Error during logout"); - return BadRequest(ApiResponse.ErrorResponse("Internal server error", ex.Message, 500)); + _logger.LogError("Unexpected error during logout : {Error}", ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error occurred", ex.Message, 500)); } } [HttpPost("refresh-token")] public async Task RefreshToken([FromBody] RefreshTokenDto refreshTokenDto) { - var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken); - if (refreshToken == null || refreshToken.ExpiryDate < DateTime.UtcNow) + if (string.IsNullOrWhiteSpace(refreshTokenDto.RefreshToken)) { - return Unauthorized(ApiResponse.ErrorResponse("Invalid or expired refresh token.", "Invalid or expired refresh token.", 401)); + _logger.LogWarning("Refresh token is missing from the request body."); + return BadRequest(ApiResponse.ErrorResponse("Refresh token is required.", "Missing refresh token.", 400)); } - // Mark token as used - await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken); + try + { + // Step 1: Fetch and validate the refresh token + var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken); + if (refreshToken == null) + { + _logger.LogWarning("Refresh token not found in the database"); + return Unauthorized(ApiResponse.ErrorResponse("Invalid or expired refresh token.", "Token not found.", 401)); + } - // Generate new JWT token and refresh token - var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty); - if (user == null) - return BadRequest(ApiResponse.ErrorResponse("Invalid request.", "Invalid request.", 400)); + if (refreshToken.ExpiryDate < DateTime.UtcNow) + { + _logger.LogWarning("Refresh token expired"); + return Unauthorized(ApiResponse.ErrorResponse("Refresh token expired.", "Token expired.", 401)); + } - Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); + // Step 2: Mark the token as used + await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken); + _logger.LogInfo("Refresh token marked as used"); - if (user.UserName == null) return NotFound(ApiResponse.ErrorResponse("UserName Not found", "UserName Not found", 404)); + // Step 3: Validate and retrieve user + var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty); + if (user == null) + { + _logger.LogWarning("User not found for RefreshToken: {Token}", refreshTokenDto.RefreshToken); + return BadRequest(ApiResponse.ErrorResponse("Invalid request.", "User not found.", 400)); + } - var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings); - var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); + if (string.IsNullOrWhiteSpace(user.UserName)) + { + _logger.LogError("Username missing for user ID: {UserId}", user.Id); + return NotFound(ApiResponse.ErrorResponse("Username not found.", "Username not found.", 404)); + } - return Ok(ApiResponse.SuccessResponse(new { token = newJwtToken, refreshToken = newRefreshToken }, "User refresh token generated successfully.", 200)); + // Step 4: Fetch employee and generate new tokens + var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); + + var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings); + var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); + + _logger.LogInfo("New access and refresh token issued for user: {UserId}", user.Id); + + return Ok(ApiResponse.SuccessResponse( + new { token = newJwtToken, refreshToken = newRefreshToken }, + "User refresh token generated successfully.", + 200)); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred during token refresh. : {Error}", ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error occurred.", ex.Message, 500)); + } } [HttpPost("forgot-password")] public async Task ForgotPassword([FromBody] ForgotPasswordDto forgotPasswordDto) { + if (string.IsNullOrWhiteSpace(forgotPasswordDto.Email)) + { + _logger.LogWarning("ForgotPassword request received without email."); + return BadRequest(ApiResponse.ErrorResponse("Email is required.", "Email is required.", 400)); + } + var user = await _userManager.FindByEmailAsync(forgotPasswordDto.Email); - if (user == null) - return NotFound(ApiResponse.ErrorResponse("User not found.", "User not found.", 404)); + if (user == null || user.Email == null) + { + _logger.LogWarning("ForgotPassword requested for non-existent or null-email user: {Email}", forgotPasswordDto.Email); + // Do not disclose whether the email exists (security best practice) + return Ok(ApiResponse.SuccessResponse(true, "Password reset link sent if the account exists.", 200)); + } - /* SEND USER REGISTRATION MAIL*/ - var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; + try + { + // Generate token and build reset link + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; - if (user.Email == null) return NotFound(ApiResponse.ErrorResponse("Email Not found", "Email Not found", 404)); + // Send reset email + await _emailSender.SendResetPasswordEmail(user.Email, user.UserName ?? "User", resetLink); - await _emailSender.SendResetPasswordEmail(user.Email, "", resetLink); - - - return Ok(ApiResponse.SuccessResponse(true, "Password reset link sent.", 200)); + _logger.LogInfo("Password reset link sent to user: {Email}", user.Email); + return Ok(ApiResponse.SuccessResponse(true, "Password reset link sent if the account exists.", 200)); + } + catch (Exception ex) + { + _logger.LogError("Error while sending password reset email to: {Error}", ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Error sending password reset email.", ex.Message, 500)); + } } [HttpPost("reset-password")] public async Task ResetPassword([FromBody] ResetPasswordDto model) { - var user = await _userManager.FindByEmailAsync(model.Email ?? string.Empty); - if (user == null) - return BadRequest(ApiResponse.ErrorResponse("Invalid request.", "Invalid request.", 400)); + _logger.LogInfo("Password reset request received for email: {Email}", model.Email ?? string.Empty); + + if (string.IsNullOrWhiteSpace(model.Email) || string.IsNullOrWhiteSpace(model.Token) || string.IsNullOrWhiteSpace(model.NewPassword)) + { + _logger.LogWarning("Reset password failed due to missing input fields for email: {Email}", model.Email ?? string.Empty); + return BadRequest(ApiResponse.ErrorResponse("All fields are required.", "Invalid input.", 400)); + } + + var user = await _userManager.FindByEmailAsync(model.Email); + if (user == null) + { + _logger.LogWarning("Reset password failed - user not found for email: {Email}", model.Email); + return BadRequest(ApiResponse.ErrorResponse("Invalid request.", "Invalid user.", 400)); + } - // var isTokenValid = await _userManager.VerifyUserTokenAsync(user,UserManager.ResetPasswordTokenPurpose, model.ResetCode); var isTokenValid = await _userManager.VerifyUserTokenAsync( - user, - TokenOptions.DefaultProvider, // This is the token provider - UserManager.ResetPasswordTokenPurpose, - WebUtility.UrlDecode(model.Token) - ); + user, + TokenOptions.DefaultProvider, // This is the token provider + UserManager.ResetPasswordTokenPurpose, + WebUtility.UrlDecode(model.Token) + ); string token = ""; if (!isTokenValid) { + _logger.LogWarning("Decoded token failed, retrying with raw token for email: {Email}", model.Email); + var isDecodedTokenValid = await _userManager.VerifyUserTokenAsync( user, - TokenOptions.DefaultProvider, // This is the token provider + TokenOptions.DefaultProvider, UserManager.ResetPasswordTokenPurpose, - model.Token + model.Token ); + if (!isDecodedTokenValid) - return BadRequest(ApiResponse.ErrorResponse("Invalid request.", "Invalid request.", 400)); + { + _logger.LogWarning("Both decoded and raw token failed for email: {Email}", model.Email); + return BadRequest(ApiResponse.ErrorResponse("Invalid request.", "Invalid or expired reset token.", 400)); + } token = model.Token; } @@ -263,26 +461,31 @@ namespace MarcoBMS.Services.Controllers token = WebUtility.UrlDecode(model.Token); } - - var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword ?? string.Empty); + var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword); if (!result.Succeeded) { var errors = result.Errors.Select(e => e.Description).ToList(); - return BadRequest(ApiResponse.ErrorResponse("Failed to Change password", errors, 400)); + _logger.LogWarning("Reset password failed for user: {Email}. Errors: {Errors}", model.Email, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Failed to reset password.", errors, 400)); } try { - Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); - await _emailSender.SendResetPasswordSuccessEmail(user.Email ?? string.Empty, emp.FirstName + " " + emp.LastName); + var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); + string fullName = $"{emp.FirstName} {emp.LastName}".Trim(); + + await _emailSender.SendResetPasswordSuccessEmail(user.Email!, fullName); + + _logger.LogInfo("Reset password success email sent to user: {Email}", model.Email); } catch (Exception ex) { - return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex.Message, 400)); + _logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message); + // Continue, do not fail because of email issue } - - return Ok(ApiResponse.SuccessResponse(result.Succeeded, "Password reset successfully.", 200)); + _logger.LogInfo("Password reset successful for user: {Email}", model.Email); + return Ok(ApiResponse.SuccessResponse(true, "Password reset successfully.", 200)); } [HttpPost("send-otp")]