using AutoMapper; 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.Model.ViewModels.Tenant; using Marco.Pms.Services.Helpers; 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 IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly UserManager _userManager; private readonly JwtSettings _jwtSettings; private readonly IConfiguration _configuration; private readonly ILoggingService _logger; public AuthController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, UserManager userManager, JwtSettings jwtSettings, IConfiguration configuration, ILoggingService logger) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _jwtSettings = jwtSettings ?? throw new ArgumentNullException(nameof(jwtSettings)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } // old login APIs [HttpPost("login/v1")] public async Task Login([FromBody] LoginDto loginDto) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); var _employeeHelper = scope.ServiceProvider.GetRequiredService(); 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 ?? Guid.Empty, emp.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); var _employeeHelper = scope.ServiceProvider.GetRequiredService(); try { // Validate input DTO if (loginDto == null || string.IsNullOrWhiteSpace(loginDto.Username) || string.IsNullOrWhiteSpace(loginDto.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 ?? Guid.Empty); // Generate JWT token var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _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(), emp.OrganizationId, _jwtSettings); // Fetch MPIN Token var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id)); // --- 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); 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)); } 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/v1")] public async Task VerifyMPIN([FromBody] VerifyMPINDto verifyMPIN) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); 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)); 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, requestEmployee.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), requestEmployee.OrganizationId, _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("login-otp/v1")] public async Task LoginWithOTP([FromBody] VerifyOTPDto verifyOTP) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); 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) .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.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings); //var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings); var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.OrganizationId == requestEmployee.OrganizationId); var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName, tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenant?.Id.ToString(), requestEmployee.OrganizationId, _jwtSettings); // Fetch MPIN token if exists var mpinDetails = await _context.MPINDetails .FirstOrDefaultAsync(p => p.UserId == userId); // 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)); } } // new login APIs [HttpPost("login")] public async Task LoginAsync([FromBody] LoginDto loginDto) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); try { // Retrieve employee details var emp = await _context.Employees.FirstOrDefaultAsync(e => e.Email == loginDto.Username && e.IsActive && e.HasApplicationAccess); if (emp == null) { _logger.LogWarning("Login failed: No employee record found for Email: {Email}", loginDto.Username ?? string.Empty); return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 404)); } // Find user by email 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 Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 400)); } // Ensure the user's email is confirmed if (!user.EmailConfirmed) { _logger.LogWarning("Login failed: Email not confirmed for UserId: {UserId}", user.Id); return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 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)); } // Ensure UserName exists for JWT if (string.IsNullOrWhiteSpace(user.UserName)) { _logger.LogWarning("Login failed: Username not found for UserId: {UserId}", user.Id); return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 404)); } // Generate tokens var token = _refreshTokenService.GenerateJwtTokenWithOrganization(user.UserName, emp.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(user.Id, emp.OrganizationId, _jwtSettings); //var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, _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)); } } [HttpPost("app/login")] public async Task LoginMobileAsync([FromBody] LoginDto loginDto) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); var _employeeHelper = scope.ServiceProvider.GetRequiredService(); // Validate input DTO if (loginDto == null || string.IsNullOrWhiteSpace(loginDto.Username) || string.IsNullOrWhiteSpace(loginDto.Password)) { return BadRequest(ApiResponse.ErrorResponse("Username or password is missing.", "Invalid request", 400)); } // Find user by email or phone number var user = await _context.ApplicationUsers .FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username); // If user not found, return unauthorized if (user == null) { return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401)); } // Check if user is inactive if (!user.IsActive) { return BadRequest(ApiResponse.ErrorResponse("User is inactive", "User is inactive", 400)); } // Check if user email is not confirmed if (!user.EmailConfirmed) { return BadRequest(ApiResponse.ErrorResponse("Your email is not verified. Please verify your email.", "Email not verified", 400)); } // Validate password using ASP.NET Identity var isPasswordValid = await _userManager.CheckPasswordAsync(user, loginDto.Password); if (!isPasswordValid) { return Unauthorized(ApiResponse.ErrorResponse("Invalid username or password.", "Invalid credentials", 401)); } // Check if username is missing if (string.IsNullOrWhiteSpace(user.UserName)) { return NotFound(ApiResponse.ErrorResponse("UserName not found", "Username is missing", 404)); } // Get employee information for tenant context var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); if (emp == null) { return NotFound(ApiResponse.ErrorResponse("Employee not found", "Employee details missing", 404)); } // Generate JWT token var token = _refreshTokenService.GenerateJwtTokenWithOrganization(user.UserName, emp.OrganizationId, _jwtSettings); // Generate Refresh Token and store in DB var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(user.Id, emp.OrganizationId, _jwtSettings); //// Generate JWT token //var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, _jwtSettings); //// Generate Refresh Token and store in DB //var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); // Fetch MPIN Token var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id)); // Combine all tokens in response var responseData = new { token, refreshToken, mpinToken = mpinToken?.MPINToken }; // Return success response return Ok(ApiResponse.SuccessResponse(responseData, "User logged in successfully.", 200)); } [HttpPost("login-mpin")] public async Task VerifyMPINAsync([FromBody] VerifyMPINDto verifyMPIN) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); 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? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; // Validate essential claims if (string.IsNullOrWhiteSpace(tokenType) || 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)); } // Fetch employee by ID and tenant var requestEmployee = await _context.Employees .Include(e => e.ApplicationUser) .FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.HasApplicationAccess && 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)); 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.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings); //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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); var _userHelper = scope.ServiceProvider.GetRequiredService(); var tenantId = _userHelper.GetTenantId(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); 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"); } if (!string.IsNullOrWhiteSpace(logoutDto.FcmToken)) { var fcmTokenMapping = await _context.FCMTokenMappings.FirstOrDefaultAsync(ft => ft.EmployeeId == loggedInEmployee.Id && ft.FcmToken == logoutDto.FcmToken && ft.TenantId == tenantId); if (fcmTokenMapping != null) { _context.FCMTokenMappings.Remove(fcmTokenMapping); await _context.SaveChangesAsync(); } } _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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); var _employeeHelper = scope.ServiceProvider.GetRequiredService(); 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 { // Validate the MPIN token and extract claims var claimsPrincipal = _refreshTokenService.ValidateToken(refreshTokenDto.RefreshToken, _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? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value ?? string.Empty; var tenantId = Guid.Parse(tokenTenantId); // 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, tenantId, emp.OrganizationId, _jwtSettings); var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenantId.ToString(), emp.OrganizationId, _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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _emailSender = scope.ServiceProvider.GetRequiredService(); 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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _emailSender = scope.ServiceProvider.GetRequiredService(); var _employeeHelper = scope.ServiceProvider.GetRequiredService(); _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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _emailSender = scope.ServiceProvider.GetRequiredService(); 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 = GenerateSecureOtp(); // Store OTP in database var otpDetails = new OTPDetails { UserId = Guid.Parse(requestedUser.Id), OTP = otp, ExpriesInSec = 300, // 5 minutes TimeStamp = DateTime.UtcNow }; _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 LoginWithOTPAsync([FromBody] VerifyOTPDto verifyOTP) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); 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) .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.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings); //var accessToken = _refreshTokenService.GenerateJwtToken( // requestEmployee.ApplicationUser?.UserName, // requestEmployee.TenantId ?? Guid.Empty, // _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); // 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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _emailSender = scope.ServiceProvider.GetRequiredService(); 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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); try { var _userHelper = scope.ServiceProvider.GetRequiredService(); var _emailSender = scope.ServiceProvider.GetRequiredService(); var _employeeHelper = scope.ServiceProvider.GetRequiredService(); // 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) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _userHelper = scope.ServiceProvider.GetRequiredService(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); 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); // 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.OrganizationId.ToString(), _jwtSettings ); // Prepare MPIN entity Guid userId = Guid.Parse(requestEmployee.ApplicationUserId ?? string.Empty); var existingMPIN = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == userId); if (existingMPIN == null) { // Add new MPIN record var mPINDetails = new MPINDetails { UserId = userId, MPIN = mpinHash, MPINToken = mpinToken, TimeStamp = DateTime.UtcNow }; _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) { // Create DbContext asynchronously for fresh connection await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Create a scope to get scoped services using var scope = _serviceScopeFactory.CreateScope(); var _httpContextAccessor = scope.ServiceProvider.GetRequiredService(); var _userHelper = scope.ServiceProvider.GetRequiredService(); var tenantId = _userHelper.GetTenantId(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var existingFCMTokenMapping = await _context.FCMTokenMappings.Where(ft => ft.FcmToken == model.FcmToken).ToListAsync(); if (existingFCMTokenMapping.Any()) { _context.FCMTokenMappings.RemoveRange(existingFCMTokenMapping); } var fcmTokenMapping = new FCMTokenMapping { EmployeeId = loggedInEmployee.Id, FcmToken = model.FcmToken, ExpiredAt = DateTime.UtcNow.AddDays(6), TenantId = tenantId }; _context.FCMTokenMappings.Add(fcmTokenMapping); _logger.LogInfo("New FCM Token registering 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)); } [Authorize] [HttpGet("get/user/tenants")] public async Task GetTenantListByEmployeeAsync() { // Create DbContext asynchronously to ensure DB connection is established fresh await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Create a service scope to resolve scoped services like IHttpContextAccessor, IMapper, ILogger using var scope = _serviceScopeFactory.CreateScope(); var _httpContextAccessor = scope.ServiceProvider.GetRequiredService(); var _mapper = scope.ServiceProvider.GetRequiredService(); var _userHelper = scope.ServiceProvider.GetRequiredService(); _logger.LogDebug("Starting GetTenantAsync method."); // Extract OrganizationId from current user's claims string stringOrganizationId = _httpContextAccessor.HttpContext?.User.FindFirst("OrganizationId")?.Value ?? string.Empty; Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (string.IsNullOrEmpty(stringOrganizationId)) { _logger.LogWarning("OrganizationId claim missing in user token."); return BadRequest(ApiResponse.ErrorResponse("OrganizationId claim is missing", 400)); } if (!Guid.TryParse(stringOrganizationId, out var organizationId)) { _logger.LogWarning("Invalid OrganizationId format: {OrganizationId}", stringOrganizationId); return BadRequest(ApiResponse.ErrorResponse("Invalid OrganizationId format", 400)); } _logger.LogInfo("Fetching TenantOrgMappings for OrganizationId: {OrganizationId}", organizationId); // Retrieve all TenantOrgMappings that match the organizationId and have a related Tenant var tenantOrgMappingTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.TenantOrgMappings.Where(to => to.OrganizationId == organizationId && to.Tenant != null).Select(to => to.TenantId).ToListAsync(); }); var projectTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Projects.Where(to => to.PromoterId == organizationId || to.PMCId == organizationId).Select(to => to.TenantId).ToListAsync(); }); await Task.WhenAll(tenantOrgMappingTask, projectTask); var tenantIds = tenantOrgMappingTask.Result; tenantIds.AddRange(projectTask.Result); tenantIds = tenantIds.Distinct().ToList(); // Additionally fetch the Tenant record associated directly with this OrganizationId if any var tenants = await _context.Tenants .Include(t => t.Industry) .Include(t => t.TenantStatus) .Where(t => t.OrganizationId == organizationId || tenantIds.Contains(t.Id)).ToListAsync(); tenants = tenants.Distinct().ToList(); // Map the tenant entities to TenantListVM view models var response = _mapper.Map>(tenants); _logger.LogInfo("Fetched {Count} tenants for OrganizationId: {OrganizationId}", tenants.Count, organizationId); _logger.LogDebug("GetTenantAsync method completed successfully."); return Ok(ApiResponse.SuccessResponse(response, "Successfully fetched the list of tenant", 200)); } [Authorize] [HttpPost("select-tenant/{tenantId}")] public async Task SelectTenantAsync(Guid tenantId) { // Create DbContext asynchronously for fresh connection await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Create a scope to get scoped services using var scope = _serviceScopeFactory.CreateScope(); var _httpContextAccessor = scope.ServiceProvider.GetRequiredService(); var _userHelper = scope.ServiceProvider.GetRequiredService(); var _refreshTokenService = scope.ServiceProvider.GetRequiredService(); _logger.LogDebug("Starting SelectTenantAsync for tenantId: {TenantId}", tenantId); // Get the current logged-in employee Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Extract OrganizationId from user claims string stringOrganizationId = _httpContextAccessor.HttpContext?.User.FindFirst("OrganizationId")?.Value ?? string.Empty; if (string.IsNullOrEmpty(stringOrganizationId)) { _logger.LogWarning("OrganizationId claim is missing."); return BadRequest(ApiResponse.ErrorResponse("OrganizationId claim is missing", 400)); } if (!Guid.TryParse(stringOrganizationId, out var organizationId)) { _logger.LogWarning("Invalid OrganizationId format: {OrganizationId}", stringOrganizationId); return BadRequest(ApiResponse.ErrorResponse("Invalid OrganizationId format", 400)); } // Find TenantOrgMapping for given tenantId and organizationId to validate access var tenantOrganizationTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.TenantOrgMappings .FirstOrDefaultAsync(to => to.TenantId == tenantId && to.OrganizationId == organizationId); }); var primaryOrganizationTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Tenants .Where(t => t.Id == tenantId && t.OrganizationId == organizationId).ToListAsync(); }); await Task.WhenAll(tenantOrganizationTask, primaryOrganizationTask); var tenantOrganization = tenantOrganizationTask.Result; var primaryOrganization = primaryOrganizationTask.Result; if (tenantOrganization == null && !primaryOrganization.Any()) { _logger.LogWarning("Tenant Organization Mapping not found for TenantId: {TenantId} and OrganizationId: {OrganizationId}", tenantId, organizationId); return NotFound(ApiResponse.ErrorResponse("Tenant Organization Mapping not found", "Tenant Organization Mapping not found in database", 404)); } // 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"); } // Generate JWT token scoped to selected tenant and logged-in employee var token = _refreshTokenService.GenerateJwtToken(loggedInEmployee.Email, tenantId, loggedInEmployee.OrganizationId, _jwtSettings); // Generate and store refresh token var refreshToken = await _refreshTokenService.CreateRefreshToken(loggedInEmployee.ApplicationUserId, tenantId.ToString(), loggedInEmployee.OrganizationId, _jwtSettings); var _cache = scope.ServiceProvider.GetRequiredService(); await _cache.ClearAllEmployeesFromCacheByOnlyEmployeeId(loggedInEmployee.Id); _logger.LogInfo("Tenant selected and tokens generated for TenantId: {TenantId} and Employee: {EmployeeEmail}", tenantId, loggedInEmployee.Email ?? string.Empty); // Return success response including tokens return Ok(ApiResponse.SuccessResponse(new { Token = token, RefreshToken = refreshToken }, "Tenant is selected", 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(); } } private static string GenerateSecureOtp() { var randomNumber = new byte[2]; // 2 bytes can store values up to 65535 using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); } // Convert to int and restrict to 4 digit range (0-9999) int randomValue = BitConverter.ToUInt16(randomNumber, 0) % 10000; // Format with leading zeros if necessary to ensure 4 digits return randomValue.ToString("D4"); } } }