using FirebaseAdmin.Messaging; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Dtos.Authentication; using Marco.Pms.Model.Dtos.Util; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Net; using System.Security.Claims; using System.Security.Cryptography; using System.Text; namespace MarcoBMS.Services.Controllers { [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly UserManager _userManager; private readonly UserHelper _userHelper; private readonly ApplicationDbContext _context; private readonly JwtSettings _jwtSettings; private readonly RefreshTokenService _refreshTokenService; private readonly IEmailSender _emailSender; private readonly IConfiguration _configuration; private readonly EmployeeHelper _employeeHelper; private readonly ILoggingService _logger; private readonly IFirebaseService _firebase; //string tenentId = "1"; public AuthController(UserManager userManager, ApplicationDbContext context, JwtSettings jwtSettings, RefreshTokenService refreshTokenService, IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper, UserHelper userHelper, ILoggingService logger, IFirebaseService firebase) { _userManager = userManager; _jwtSettings = jwtSettings; _refreshTokenService = refreshTokenService; _emailSender = emailSender; _configuration = configuration; _employeeHelper = employeeHelper; _context = context; _userHelper = userHelper; _logger = logger; _firebase = firebase; } [HttpPost("login")] public async Task Login([FromBody] LoginDto loginDto) { try { 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) { _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) { _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)); } // Validate the password if (!await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty)) { _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)); } // 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(ex, "Unexpected error during login"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } /// /// Handles mobile user login, validates credentials, sends a test push notification, /// and generates JWT, Refresh, and MPIN tokens upon successful authentication. /// /// Data Transfer Object containing the user's login credentials and device token. /// An IActionResult containing the authentication tokens or an error response. [HttpPost("login-mobile")] public async Task LoginMobile([FromBody] LoginDto loginDto) { // Log the start of the login attempt for traceability. _logger.LogInfo("Login attempt initiated for user: {Username}", loginDto.Username ?? "N/A"); try { // --- Input Validation --- // Ensure that the request body and essential fields are not null or empty. if (loginDto == null || string.IsNullOrWhiteSpace(loginDto.Username) || string.IsNullOrWhiteSpace(loginDto.Password)) { _logger.LogWarning("Login failed due to missing username or password."); return BadRequest(ApiResponse.ErrorResponse("Username or password is missing.", "Invalid request", 400)); } // --- User Retrieval --- // Find the user in the database by their email or phone number. _logger.LogInfo("Searching for user: {Username}", loginDto.Username); var user = await _context.ApplicationUsers .FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username); // If no user is found, return an unauthorized response. if (user == null || string.IsNullOrWhiteSpace(user.UserName)) { _logger.LogWarning("Login failed: User not found for username {Username}", loginDto.Username); return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401)); } // --- User Status Checks --- // Check if the user's account is marked as inactive. if (!user.IsActive) { _logger.LogWarning("Login failed: User '{Username}' account is inactive.", user.UserName); return BadRequest(ApiResponse.ErrorResponse("User is inactive", "User is inactive", 400)); } // Check if the user has confirmed their email address. if (!user.EmailConfirmed) { _logger.LogWarning("Login failed: User '{Username}' email is not verified.", user.UserName); return BadRequest(ApiResponse.ErrorResponse("Your email is not verified. Please verify your email.", "Email not verified", 400)); } // --- Password Validation --- // Use ASP.NET Identity's UserManager to securely check the password. _logger.LogInfo("Validating password for user: {Username}", user.UserName); var isPasswordValid = await _userManager.CheckPasswordAsync(user, loginDto.Password); if (!isPasswordValid) { _logger.LogWarning("Login failed: Invalid password for user {Username}", user.UserName); return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid credentials", 401)); } _logger.LogInfo("Password validation successful for user: {Username}", user.UserName); // Check if the username property on the user object is populated. if (string.IsNullOrWhiteSpace(user.UserName)) { // This is an unlikely edge case, but good to handle. _logger.LogWarning("Login failed: User object for ID {UserId} is missing a UserName.", user.Id); return NotFound(ApiResponse.ErrorResponse("UserName not found", "Username is missing", 404)); } // --- Employee and Tenant Context Retrieval --- // Fetch associated employee details to get tenant context for token generation. _logger.LogInfo("Fetching employee details for user ID: {UserId}", user.Id); var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); if (emp == null) { _logger.LogWarning("Login failed: Could not find associated employee record for user ID {UserId}", user.Id); return NotFound(ApiResponse.ErrorResponse("Employee not found", "Employee details missing", 404)); } _logger.LogInfo("Successfully found employee details for tenant ID: {TenantId}", emp.TenantId); // --- Token Generation --- // Generate the primary JWT access token. _logger.LogInfo("Generating JWT for user: {Username}", user.UserName); var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings); // Generate a new refresh token and store it in the database. _logger.LogInfo("Generating and storing Refresh Token for user: {Username}", user.UserName); var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); // Fetch the user's MPIN token if it exists. _logger.LogInfo("Fetching MPIN token for user: {Username}", user.UserName); var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id) && p.TenantId == emp.TenantId); // --- Response Assembly --- // Combine all tokens into a single response object. var responseData = new { token, refreshToken, mpinToken = mpinToken?.MPINToken // Safely access the MPIN token, will be null if not found. }; // Return a successful response with the generated tokens. _logger.LogInfo("User {Username} logged in successfully.", user.UserName); var exsistingFCMMapping = await _context.FCMTokenMappings.FirstOrDefaultAsync(ft => ft.EmployeeId == emp.Id); if (exsistingFCMMapping == null) { var fcmTokenMapping = new FCMTokenMapping { EmployeeId = emp.Id, FcmToken = loginDto.FcmToken, TenantId = emp.TenantId }; _context.FCMTokenMappings.Add(fcmTokenMapping); _logger.LogInfo("New FCM Token registering for employee {EmployeeId}", emp.Id); } else { exsistingFCMMapping.FcmToken = loginDto.FcmToken; _logger.LogInfo("Updating FCM Token for employee {EmployeeId}", emp.Id); } try { await _context.SaveChangesAsync(); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while saving FCM Token for employee {EmployeeId}", emp.Id); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error", ex.Message, 500)); } _ = Task.Run(async () => { // --- Push Notification Section --- // This section attempts to send a test push notification to the user's device. // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. var notification = new Notification { Title = "Testing from API", Body = "This messages comes from FireBase Services" }; await _firebase.SendLoginMessageAsync(notification); }); return Ok(ApiResponse.SuccessResponse(responseData, "User logged in successfully.", 200)); } catch (Exception ex) { // --- Global Exception Handling --- // Catch any unexpected exceptions during the login process. _logger.LogError(ex, "An unexpected error occurred during the LoginMobile process for user: {Username}", loginDto?.Username ?? "N/A"); // Return a generic 500 Internal Server Error to avoid leaking implementation details. return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", "Server Error", 500)); } } [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(ex, "Unexpected error occurred while verifying MPIN"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } [HttpPost("logout")] public async Task Logout([FromBody] LogoutDto logoutDto) { 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 JWT access token string jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); 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, "Unexpected error during logout"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error occurred", ex.Message, 500)); } } [HttpPost("refresh-token")] public async Task RefreshToken([FromBody] RefreshTokenDto refreshTokenDto) { if (string.IsNullOrWhiteSpace(refreshTokenDto.RefreshToken)) { _logger.LogWarning("Refresh token is missing from the request body."); return BadRequest(ApiResponse.ErrorResponse("Refresh token is required.", "Missing refresh token.", 400)); } 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)); } if (refreshToken.ExpiryDate < DateTime.UtcNow) { _logger.LogWarning("Refresh token expired"); return Unauthorized(ApiResponse.ErrorResponse("Refresh token expired.", "Token expired.", 401)); } // Step 2: Mark the token as used await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken); _logger.LogInfo("Refresh token marked as used"); // 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)); } if (string.IsNullOrWhiteSpace(user.UserName)) { _logger.LogWarning("Username missing for user ID: {UserId}", user.Id); return NotFound(ApiResponse.ErrorResponse("Username not found.", "Username not found.", 404)); } // 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(ex, "An unexpected error occurred during token refresh."); 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 || 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)); } try { // Generate token and build reset link var token = await _userManager.GeneratePasswordResetTokenAsync(user); var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; // Send reset email await _emailSender.SendResetPasswordEmail(user.Email, user.UserName ?? "User", resetLink); _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(ex, "Error while sending password reset email to"); return StatusCode(500, ApiResponse.ErrorResponse("Error sending password reset email.", ex.Message, 500)); } } [HttpPost("reset-password")] public async Task ResetPassword([FromBody] ResetPasswordDto model) { _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, 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, UserManager.ResetPasswordTokenPurpose, model.Token ); if (!isDecodedTokenValid) { _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; } else { token = WebUtility.UrlDecode(model.Token); } var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword); if (!result.Succeeded) { var errors = result.Errors.Select(e => e.Description).ToList(); _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 { 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) { _logger.LogError(ex, "Error while sending reset password success email to user"); // Continue, do not fail because of email issue } _logger.LogInfo("Password reset successful for user: {Email}", model.Email); return Ok(ApiResponse.SuccessResponse(true, "Password reset successfully.", 200)); } [HttpPost("send-otp")] public async Task SendOtpEmail([FromBody] GenerateOTPDto generateOTP) { try { // Validate input email if (string.IsNullOrWhiteSpace(generateOTP.Email)) { _logger.LogWarning("Send OTP failed - Email is missing"); return BadRequest(ApiResponse.ErrorResponse("Email is required", "Invalid email", 400)); } // Fetch user by email var requestedUser = await _userManager.FindByEmailAsync(generateOTP.Email); string title = "Send OTP"; if (requestedUser != null && requestedUser.IsActive) { // Fetch employee details var requestedEmployee = await _context.Employees .FirstOrDefaultAsync(e => e.ApplicationUserId == requestedUser.Id); // Generate a random 4-digit OTP string otp = new Random().Next(1000, 9999).ToString(); // Store OTP in database var otpDetails = new OTPDetails { UserId = Guid.Parse(requestedUser.Id), OTP = otp, ExpriesInSec = 600, // 10 minutes TimeStamp = DateTime.UtcNow, TenantId = requestedUser.TenantId }; _context.OTPDetails.Add(otpDetails); await _context.SaveChangesAsync(); // Prepare email List toEmails = [generateOTP.Email]; string name = $"{requestedEmployee?.FirstName} {requestedEmployee?.LastName}".Trim(); var mailTemplate = await _context.MailingList .FirstOrDefaultAsync(t => t.Title.ToLower() == title.ToLower()); string subject = mailTemplate?.Subject ?? string.Empty; string emailBody = mailTemplate?.Body ?? string.Empty; // Send OTP via email await _emailSender.SendOTP(toEmails, emailBody, name, otp, subject); _logger.LogInfo("OTP sent successfully to {Email}", generateOTP.Email); return Ok(ApiResponse.SuccessResponse("Success", "OTP generated successfully", 200)); } _logger.LogWarning("Send OTP failed - Invalid or inactive user: {Email}", generateOTP.Email); return BadRequest(ApiResponse.ErrorResponse("Provided invalid information", "User not found or inactive", 400)); } catch (Exception ex) { _logger.LogError(ex, "An unexpected error occurred while sending OTP to {Email}", generateOTP.Email ?? ""); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred.", ex.Message, 500)); } } [HttpPost("login-otp")] public async Task LoginWithOTP([FromBody] VerifyOTPDto verifyOTP) { try { // Validate input if (string.IsNullOrWhiteSpace(verifyOTP.Email) || string.IsNullOrWhiteSpace(verifyOTP.OTP) || verifyOTP.OTP.Length != 4 || !verifyOTP.OTP.All(char.IsDigit)) { _logger.LogWarning("OTP login failed - invalid input provided"); return BadRequest(ApiResponse.ErrorResponse("Invalid input", "Please provide a valid 4-digit OTP and Email", 400)); } // Fetch employee by email var requestEmployee = await _context.Employees .Include(e => e.ApplicationUser) .FirstOrDefaultAsync(e => e.Email == verifyOTP.Email && e.IsActive); if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId)) { _logger.LogWarning("OTP login failed - user not found for email {Email}", verifyOTP.Email); return NotFound(ApiResponse.ErrorResponse("User not found", "User not found", 404)); } Guid userId = Guid.Parse(requestEmployee.ApplicationUserId); // Fetch most recent OTP var otpDetails = await _context.OTPDetails .Where(o => o.UserId == userId && o.TenantId == requestEmployee.TenantId) .OrderByDescending(o => o.TimeStamp) .FirstOrDefaultAsync(); if (otpDetails == null) { _logger.LogWarning("OTP login failed - no OTP found for user {UserId}", userId); return NotFound(ApiResponse.ErrorResponse("OTP not found", "No OTP was generated for this user", 404)); } // Validate OTP expiration var validUntil = otpDetails.TimeStamp.AddSeconds(otpDetails.ExpriesInSec); if (DateTime.UtcNow > validUntil || otpDetails.IsUsed) { _logger.LogWarning("OTP login failed - OTP expired for user {UserId}", userId); return BadRequest(ApiResponse.ErrorResponse("OTP expired", "The OTP has expired, please request a new one", 400)); } // Match OTP if (otpDetails.OTP != verifyOTP.OTP) { _logger.LogWarning("OTP login failed - incorrect OTP entered for user {UserId}", userId); return Unauthorized(ApiResponse.ErrorResponse("Invalid OTP", "OTP did not match", 401)); } // Generate access and refresh tokens var accessToken = _refreshTokenService.GenerateJwtToken( requestEmployee.ApplicationUser?.UserName, requestEmployee.TenantId, _jwtSettings ); var refreshToken = await _refreshTokenService.CreateRefreshToken( requestEmployee.ApplicationUserId, requestEmployee.TenantId.ToString(), _jwtSettings ); // Fetch MPIN token if exists var mpinDetails = await _context.MPINDetails .FirstOrDefaultAsync(p => p.UserId == userId && p.TenantId == requestEmployee.TenantId); // Build and return response var response = new { token = accessToken, refreshToken, mpinToken = mpinDetails?.MPINToken }; otpDetails.IsUsed = true; await _context.SaveChangesAsync(); _logger.LogInfo("OTP login successful for employee {EmployeeId}", requestEmployee.Id); return Ok(ApiResponse.SuccessResponse(response, "User logged in successfully.", 200)); } catch (Exception ex) { _logger.LogError(ex, "An unexpected error occurred during OTP login for email {Email}", verifyOTP.Email ?? string.Empty); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } [HttpPost("sendmail")] public async Task SendEmail([FromBody] EmailDot emailDot) { var user = await _userManager.FindByEmailAsync(emailDot.ToEmail ?? string.Empty); if (user == null) { return NotFound(ApiResponse.ErrorResponse("User not found.", "User not found.", 404)); } /* New User*/ //var token = await _userManager.GeneratePasswordResetTokenAsync(user); //var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; //await _emailSender.SendResetPasswordEmailOnRegister(emailDot.ToEmail, "Vikas", resetLink); /* Forget password*/ // var token = await _userManager.GeneratePasswordResetTokenAsync(user); var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword"); var isTokenValid = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", token); 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)); await _emailSender.SendResetPasswordEmail(user.Email, "", resetLink); return Ok(ApiResponse.SuccessResponse(new { }, "Password reset link sent.", 200)); } [Authorize] [HttpPost("change-password")] public async Task ChangePassword([FromBody] ChangePasswordDto changePassword) { try { // Get the currently logged-in user var loggedUser = await _userHelper.GetCurrentUserAsync(); // Validate email if (string.IsNullOrWhiteSpace(changePassword.Email)) { _logger.LogWarning("Change password attempt failed - Email is missing"); return BadRequest(ApiResponse.ErrorResponse("Email is missing", "Email is missing", 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.LogWarning("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) { _logger.LogError(exp, "An unexpected error occurred while changing password"); 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) || generateMPINDto.MPIN.Length != 4 || !generateMPINDto.MPIN.All(char.IsDigit)) { _logger.LogWarning("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)); } } [Authorize] [HttpPost("set/device-token")] public async Task StoreDeviceToken([FromBody] FCMTokenDto model) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var tenantId = _userHelper.GetTenantId(); var exsistingFCMMapping = await _context.FCMTokenMappings.FirstOrDefaultAsync(ft => ft.EmployeeId == loggedInEmployee.Id); if (exsistingFCMMapping == null) { var fcmTokenMapping = new FCMTokenMapping { EmployeeId = loggedInEmployee.Id, FcmToken = model.FcmToken, TenantId = tenantId }; _context.FCMTokenMappings.Add(fcmTokenMapping); _logger.LogInfo("New FCM Token registering for employee {EmployeeId}", loggedInEmployee.Id); } else { exsistingFCMMapping.FcmToken = model.FcmToken; _logger.LogInfo("Updating FCM Token for employee {EmployeeId}", loggedInEmployee.Id); } try { await _context.SaveChangesAsync(); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while saving FCM Token for employee {EmployeeId}", loggedInEmployee.Id); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error", ex.Message, 500)); } return Ok(ApiResponse.SuccessResponse(new { }, "FCM Token registered Successfuly", 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(); } } } }