using AuthHex.Infra.UoW; using AuthHex.Interfaces; using AuthHex.Models; using AuthHex.Models.IOs; using AuthHex.Repos; using AuthHex.Utility; using AuthHex.Utility.passwordHasher; using AuthSystem.API.Utils; using Core_Archi.Helpers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using static System.Runtime.InteropServices.JavaScript.JSType; namespace AuthHex.Services.UserManager; public class UserManagerService { private readonly IUserManageRepository _repository; private readonly IRecoveryManagerRepository _recoveryRepository; private readonly IUnitOfWork _uow; private readonly JwtTokenHelper _jwt; private readonly AppDBContext _dbContext; private readonly ThirdPartyService _thirdPartyService; private readonly UserHelper _userHelper; public UserManagerService(IUserManageRepository repository, IRecoveryManagerRepository recoveryRepository, IUnitOfWork uow, JwtTokenHelper jwt, AppDBContext dbContext, ThirdPartyService thirdPartyService, UserHelper userHelper) { _repository = repository; _recoveryRepository = recoveryRepository; _uow = uow; _jwt = jwt; _dbContext = dbContext; _thirdPartyService = thirdPartyService; _userHelper = userHelper; } public async Task RegisterUser(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString(); var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); var data = DeserializePayload.Deserialize(payload); //check payload data if (data == null || !data.Any()) throw new ArgumentException("Invalid payload data"); Guid userID = Guid.Parse(data.ContainsKey("userId") ? data["userId"].GetString() : throw new Exception("user id need")); var FullName = data.ContainsKey("fullname") ? data["fullname"].GetString() : null; var UserName = data.ContainsKey("userName") ? data["userName"].GetString() : null; var Nic = data.ContainsKey("nic") ? data["nic"].GetString() : null; var Email = data.ContainsKey("email") ? data["email"].GetString() : null; var MobileNumber = data.ContainsKey("mobileNumber") ? data["mobileNumber"].GetString() : null; var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device"; if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var RoleId)) throw new ArgumentException("Invalid roleId"); if (!data.ContainsKey("userTypeId") || !Guid.TryParse(data["userTypeId"].GetString(), out var UserTypeId)) throw new ArgumentException("Invalid userTypeId"); //chk user exist if (data.ContainsKey("chkUser") == true) { var userToCheck = new User { FullName = FullName, Email = Email, MobileNumber = MobileNumber, Nic = Nic, RoleId = RoleId, UserTypeId = UserTypeId }; bool canSave = await CanRegisterUser(userToCheck); if (!canSave) throw new InvalidOperationException("User already exists"); } // if send password empty generate random password if (!data.ContainsKey("password") || string.IsNullOrWhiteSpace(data["password"].GetString())) { data["password"] = JsonDocument.Parse($"\"{Guid.NewGuid().ToString().Substring(0, 8)}\"").RootElement; } var RawPassword = data["password"].GetString()!; var PasswordHash = PasswordHasher.Hash(RawPassword); var sendCredentialsEmail = !data.ContainsKey("sendCredentialsEmail") || data["sendCredentialsEmail"].GetBoolean(); // Create User object User user = new User { Id = userID, FullName = FullName, UserName = UserName, Nic = Nic, Email = Email, MobileNumber = MobileNumber, RoleId = RoleId, UserTypeId = UserTypeId, CreatedAt = DateTime.UtcNow, IsActive = true, IsLocked = false, EmailVerified = false, MobileNumberVerified = false, PasswordHash = PasswordHash }; await _repository.AddUserAsync(user); var jwtToken = await _jwt.GenerateToken(user); var refreshToken = Guid.NewGuid().ToString(); var refreshTokenHash = PasswordHasher.Hash(refreshToken); var jwtTokenHash = PasswordHasher.Hash(jwtToken); var token = new Token { UserId = user.Id, TokenHash = jwtToken }; var userSession = new UserSession { UserId = user.Id, RefreshTokenHash = refreshTokenHash, DeviceName = deviceName, IPAddress = ipAddress, Browser = _userHelper.ExtractBrowser(userAgent), OS = _userHelper.ExtractOS(userAgent), ExpiresAt = DateTime.UtcNow.AddDays(30), CreatedAt = DateTime.UtcNow }; _dbContext.UserSessions.Add(userSession); _dbContext.Token.Add(token); await LogAuthEvent(user.Id, "LOGIN_SUCCESS", ipAddress, userAgent); await _uow.CommitAsync(); if (sendCredentialsEmail && !string.IsNullOrWhiteSpace(Email)) { try { await _thirdPartyService.EmailConfiguration( Email, RawPassword, "Your ERP System Account", $@"

Your ERP System account has been created

Username: {WebUtility.HtmlEncode(UserName ?? Email)}

Temporary password: {{code}}

Please log in and change your password as soon as possible.

"); } catch { // Best-effort: account creation already committed; do not fail the request on email delivery issues. } } return new { AccessToken = jwtToken, RefreshToken = refreshToken, ExpiresIn = 3600, User = new { user.Id, user.FullName, user.UserName, user.Email, user.MobileNumber, user.EmailVerified, user.MobileNumberVerified, user.IsMfaEnabled, RoleId = user.RoleId, UserTypeId = user.UserTypeId } }; } catch { await _uow.RollbackAsync(); throw; } } public async Task LoginUser(object payload, HttpContext httpContext) { var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString(); var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var identifier = data.ContainsKey("identifier") ? data["identifier"].GetString() : null; var password = data.ContainsKey("password") ? data["password"].GetString() : null; var userTypeId = data.ContainsKey("userTypeId") && Guid.TryParse(data["userTypeId"].GetString(), out var utid) ? utid : (Guid?)null; var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device"; if (string.IsNullOrWhiteSpace(identifier)) throw new ArgumentException("Identifier and password are required"); var user = await _repository.GetUserByIdentifierAndType(identifier, userTypeId); if (user is null) { await LogAuthEvent(null, "LOGIN_FAILED", ipAddress, userAgent); throw new ArgumentException("Invalid credentials"); } if (user.IsLocked == true) { await LogAuthEvent(user.Id, "LOGIN_FAILED_LOCKED", ipAddress, userAgent); throw new ArgumentException("Account is locked"); } if (user.IsActive == false) { await LogAuthEvent(user.Id, "LOGIN_FAILED_INACTIVE", ipAddress, userAgent); throw new ArgumentException("Account is deactivated"); } if (string.IsNullOrEmpty(user.PasswordHash) || !PasswordHasher.Verify(password, user.PasswordHash)) { await LogAuthEvent(user.Id, "LOGIN_FAILED", ipAddress, userAgent); throw new ArgumentException("Invalid credentials"); } var jwtToken = await _jwt.GenerateToken(user); var refreshToken = Guid.NewGuid().ToString(); var refreshTokenHash = PasswordHasher.Hash(refreshToken); var userSession = new UserSession { UserId = user.Id, RefreshTokenHash = refreshTokenHash, DeviceName = deviceName, IPAddress = ipAddress, Browser = _userHelper.ExtractBrowser(userAgent), OS = _userHelper.ExtractOS(userAgent), ExpiresAt = DateTime.UtcNow.AddDays(30), CreatedAt = DateTime.UtcNow }; _dbContext.UserSessions.Add(userSession); await LogAuthEvent(user.Id, "LOGIN_SUCCESS", ipAddress, userAgent); await _uow.CommitAsync(); return new { AccessToken = jwtToken, RefreshToken = refreshToken, ExpiresIn = 3600, User = new { user.Id, user.FullName, user.UserName, user.Email, user.MobileNumber, user.EmailVerified, user.MobileNumberVerified, user.IsMfaEnabled, RoleId = user.RoleId, UserTypeId = user.UserTypeId } }; } catch { await _uow.RollbackAsync(); throw; } } public async Task VerifyOtpForLogin(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString(); var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); var data = DeserializePayload.Deserialize(payload); var referenceNumber = data.ContainsKey("referenceNumber") ? data["referenceNumber"].GetString() : null; var otpCode = data.ContainsKey("otpCode") ? data["otpCode"].GetString() : null; var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device"; if (string.IsNullOrEmpty(referenceNumber) || string.IsNullOrEmpty(otpCode)) throw new ArgumentException("Reference number and OTP code are required."); var recovery = await _recoveryRepository.GetRecoveryByReferenceNumAsync(referenceNumber); if (recovery == null) throw new KeyNotFoundException("Invalid reference number."); if (recovery.IsUsed) throw new InvalidOperationException("This OTP has already been used."); if (recovery.Status != "Pending") throw new InvalidOperationException("This OTP is no longer valid."); if (DateTime.UtcNow > recovery.ExpirationTime) { recovery.Status = "Expired"; await _recoveryRepository.UpdateRecoveryAsync(recovery); throw new InvalidOperationException("This OTP has expired. Please request a new one."); } if (recovery.OTP != otpCode) throw new InvalidOperationException("Invalid OTP code."); recovery.Status = "Verified"; recovery.IsUsed = true; await _recoveryRepository.UpdateRecoveryAsync(recovery); if (!recovery.UserId.HasValue) throw new KeyNotFoundException("No user linked to this OTP."); var user = await _repository.GetUserByIdAsync(recovery.UserId.Value); if (user is null) throw new KeyNotFoundException("User not found."); var jwtToken = await _jwt.GenerateToken(user); var refreshToken = Guid.NewGuid().ToString(); var refreshTokenHash = PasswordHasher.Hash(refreshToken); var userSession = new UserSession { UserId = user.Id, RefreshTokenHash = refreshTokenHash, DeviceName = deviceName, IPAddress = ipAddress, Browser = _userHelper.ExtractBrowser(userAgent), OS = _userHelper.ExtractOS(userAgent), ExpiresAt = DateTime.UtcNow.AddDays(30), CreatedAt = DateTime.UtcNow }; _dbContext.UserSessions.Add(userSession); await LogAuthEvent(user.Id, "LOGIN_SUCCESS", ipAddress, userAgent); await _uow.CommitAsync(); return new { Success = true, Message ="OTP verified successfully.", Data = new { ReferenceNumber = recovery.RecoveryReferenceNum, UserId = user.Id, Verified = true, AccessToken = jwtToken, RefreshToken = refreshToken, ExpiresIn = 3600, User = new { user.Id, user.FullName, user.UserName, user.Email, user.MobileNumber, user.EmailVerified, user.MobileNumberVerified, user.IsMfaEnabled, RoleId = user.RoleId, UserTypeId = user.UserTypeId } } }; } catch { await _uow.RollbackAsync(); throw; } } public async Task RefreshToken(object payload, HttpContext httpContext) { var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString(); var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var refreshToken = data.ContainsKey("refreshToken") ? data["refreshToken"].GetString() : null; var requestedDeviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : null; if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentException("refreshToken is required"); var existingSession = await _repository.GetActiveSessionByRefreshTokenAsync(refreshToken); if (existingSession == null) { await LogAuthEvent(null, "REFRESH_FAILED", ipAddress, userAgent); throw new ArgumentException("Invalid or expired refresh token"); } var user = existingSession.User; if (user == null) throw new ArgumentException("Invalid session user"); if (user.IsLocked == true) { await LogAuthEvent(user.Id, "REFRESH_FAILED_LOCKED", ipAddress, userAgent); throw new ArgumentException("Account is locked"); } if (user.IsActive == false) { await LogAuthEvent(user.Id, "REFRESH_FAILED_INACTIVE", ipAddress, userAgent); throw new ArgumentException("Account is deactivated"); } existingSession.RevokedAt = DateTime.UtcNow; var newAccessToken = await _jwt.GenerateToken(user); var newRefreshToken = Guid.NewGuid().ToString(); var newRefreshTokenHash = PasswordHasher.Hash(newRefreshToken); var rotatedSession = new UserSession { UserId = user.Id, RefreshTokenHash = newRefreshTokenHash, DeviceName = string.IsNullOrWhiteSpace(requestedDeviceName) ? existingSession.DeviceName : requestedDeviceName, IPAddress = ipAddress, Browser = _userHelper.ExtractBrowser(userAgent), OS = _userHelper.ExtractOS(userAgent), ExpiresAt = DateTime.UtcNow.AddDays(30), CreatedAt = DateTime.UtcNow }; _dbContext.UserSessions.Add(rotatedSession); await LogAuthEvent(user.Id, "REFRESH_SUCCESS", ipAddress, userAgent); await _uow.CommitAsync(); return new { AccessToken = newAccessToken, RefreshToken = newRefreshToken, ExpiresIn = 3600, User = new { user.Id, user.FullName, user.UserName, user.Email, user.MobileNumber, user.EmailVerified, user.MobileNumberVerified, user.IsMfaEnabled, RoleId = user.RoleId, UserTypeId = user.UserTypeId } }; } catch { await _uow.RollbackAsync(); throw; } } public async Task GetUserDetails(object payload , HttpContext httpContext) { try { var data = DeserializePayload.Deserialize(payload); Guid userId = data.ContainsKey("userId") ? data["userId"].GetGuid() : throw new Exception("UserId required"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) { throw new ArgumentException("User not found"); } return new { user.Id, user.FullName, user.UserName, user.Email, user.Nic, user.MobileNumber, user.EmailVerified, user.MobileNumberVerified, user.IsMfaEnabled, user.IsActive, user.IsLocked, user.CreatedAt, Role = new { user.Role?.RoleId, user.Role?.Code, user.Role?.Name }, UserType = new { user.UserType?.UserTypeId, user.UserType?.Code, user.UserType?.Description } }; } catch { throw; } } public async Task ListUserTypes(HttpContext httpContext) { var userTypes = await _dbContext.UserType.OrderBy(t => t.Code).ToListAsync(); return userTypes.Select(t => new { t.UserTypeId, t.Code, t.Description }).ToList(); } public async Task GetUserSessions(HttpContext httpContext) { try { var userId = _userHelper.GetUserIdFromClaims(httpContext); var sessions = await _repository.GetUserSessionsAsync(userId); return sessions.Select(s => new { s.SessionId, s.DeviceName, s.Browser, s.OS, s.IPAddress, s.CreatedAt, s.ExpiresAt, s.RevokedAt, s.IsActive }).ToList(); } catch { throw; } } public async Task ChangeUserStatus(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var userId = _userHelper.GetUserIdFromClaims(httpContext); var isActive = data.ContainsKey("isActive") ? data["isActive"].GetBoolean() : (bool?)null; if (isActive == null) throw new ArgumentException("isActive is required"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); user.IsActive = isActive.Value; await _uow.CommitAsync(); return new { user.Id, user.FullName, user.UserName, user.Email, user.MobileNumber, user.IsActive }; } catch { await _uow.RollbackAsync(); throw; } } public async Task LockUserAccount(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var userId = _userHelper.GetUserIdFromClaims(httpContext); var isLocked = data.ContainsKey("isLocked") ? data["isLocked"].GetBoolean() : (bool?)null; if (isLocked == null) throw new ArgumentException("isLocked is required"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); user.IsLocked = isLocked.Value; user.DeletedAt = DateTime.UtcNow; await _uow.CommitAsync(); await _repository.InvalidateUserSessionsAsync(user.Id); return new { user.Id, user.FullName, user.UserName, user.Email, user.MobileNumber, user.IsLocked, user.DeletedAt }; } catch { await _uow.RollbackAsync(); throw; } } public async Task VerifyPassword(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); var userId = _userHelper.GetUserIdFromClaims(httpContext); var password = data.ContainsKey("password") ? data["password"].GetString() : null; if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Password is required"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); bool valid = !string.IsNullOrEmpty(user.PasswordHash) && PasswordHasher.Verify(password, user.PasswordHash); return new { valid }; } public async Task ChangeUserPassword(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var userId = _userHelper.GetUserIdFromClaims(httpContext); var currentPassword = data.ContainsKey("currentPassword") ? data["currentPassword"].GetString() : null; var newPassword = data.ContainsKey("newPassword") ? data["newPassword"].GetString() : null; if (string.IsNullOrWhiteSpace(currentPassword) || string.IsNullOrWhiteSpace(newPassword)) throw new ArgumentException("Current password and new password are required"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); if (string.IsNullOrEmpty(user.PasswordHash) || !PasswordHasher.Verify(currentPassword, user.PasswordHash)) { throw new ArgumentException("Current password is incorrect"); } user.PasswordHash = PasswordHasher.Hash(newPassword); await _dbContext.SaveChangesAsync(); await _repository.InvalidateUserSessionsAsync(user.Id); await _uow.CommitAsync(); return new { message = "Password changed successfully" }; } catch { await _uow.RollbackAsync(); throw; } } public async Task LogoutUser(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid userId = Guid.Parse(data.ContainsKey("userId") ? data["userId"].GetString() : throw new Exception("user id need")); await _repository.InvalidateUserSessionsAsync(userId); await _uow.CommitAsync(); return new { message = "User logged out successfully" }; } catch { await _uow.RollbackAsync(); throw; } } public async Task UpdateUser(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (data == null || !data.Any()) throw new ArgumentException("Invalid payload data"); var userId = _userHelper.GetUserIdFromClaims(httpContext); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); var updated = false; if (data.ContainsKey("fullName")) { user.FullName = data["fullName"].GetString(); updated = true; } if (data.ContainsKey("userName")) { user.UserName = data["userName"].GetString(); updated = true; } if (data.ContainsKey("nic")) { user.Nic = data["nic"].GetString(); updated = true; } if (data.ContainsKey("address")) { user.Address = data["address"].GetString(); updated = true; } if (data.ContainsKey("optional1")) { user.Optional1 = data["optional1"].GetString(); updated = true; } if (data.ContainsKey("optional2")) { user.Optional2 = data["optional2"].GetString(); updated = true; } if (data.ContainsKey("email")) { var newEmail = data["email"].ValueKind == JsonValueKind.Null ? null : data["email"].GetString(); if (!string.Equals(user.Email, newEmail, StringComparison.Ordinal)) { user.Email = newEmail; user.EmailVerified = false; } updated = true; } if (data.ContainsKey("mobileNumber")) { var newMobile = data["mobileNumber"].ValueKind == JsonValueKind.Null ? null : data["mobileNumber"].GetString(); if (!string.Equals(user.MobileNumber, newMobile, StringComparison.Ordinal)) { user.MobileNumber = newMobile; user.MobileNumberVerified = false; } updated = true; } if (data.ContainsKey("newPassword")) { var currentPassword = data.ContainsKey("currentPassword") ? data["currentPassword"].GetString() : null; var newPassword = data["newPassword"].GetString(); if (string.IsNullOrWhiteSpace(newPassword)) throw new ArgumentException("New password is required for password update"); if (string.IsNullOrEmpty(user.PasswordHash)) { user.PasswordHash = PasswordHasher.Hash(newPassword); updated = true; } else { if (string.IsNullOrWhiteSpace(currentPassword)) throw new ArgumentException("Current password is required to change existing password"); if (!PasswordHasher.Verify(currentPassword, user.PasswordHash)) throw new ArgumentException("Current password is incorrect"); user.PasswordHash = PasswordHasher.Hash(newPassword); updated = true; await _repository.InvalidateUserSessionsAsync(user.Id); } } if (!updated) throw new ArgumentException("No valid fields provided for update"); await _uow.CommitAsync(); return new { message = "User updated successfully", user = new { user.Id, user.FullName, user.UserName, user.Nic, user.Address, user.Optional1, user.Optional2, user.Email, user.EmailVerified, user.MobileNumber, user.MobileNumberVerified } }; } catch { await _uow.RollbackAsync(); throw; } } //------------------------------------2FA Functions------------------------------------------------ public async Task InitiateTwoFASetup(HttpContext httpContext) { try { var userId = _userHelper.GetUserIdFromClaims(httpContext); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); if (user.IsMfaEnabled) throw new InvalidOperationException("2FA is already enabled for this user"); if (string.IsNullOrWhiteSpace(user.Email)) throw new ArgumentException("Email is required for 2FA setup"); // Generate setup data using ThirdPartyService var setupResult = await _thirdPartyService.InitiateTwoFASetup(user.Email, user.FullName ?? user.UserName); return setupResult; } catch { throw; } } public async Task CompleteTwoFASetup(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var userId = _userHelper.GetUserIdFromClaims(httpContext); var secretKey = data.ContainsKey("secretKey") ? data["secretKey"].GetString() : null; var verificationCode = data.ContainsKey("verificationCode") ? data["verificationCode"].GetString() : null; if (string.IsNullOrWhiteSpace(secretKey) || string.IsNullOrWhiteSpace(verificationCode)) throw new ArgumentException("Secret key and verification code are required"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); if (user.IsMfaEnabled) throw new InvalidOperationException("2FA is already enabled for this user"); // Verify the code using ThirdPartyService var verificationResult = await _thirdPartyService.CompleteTwoFASetup(secretKey, verificationCode, user.Email); if (verificationResult.StatusCode != 200) throw new ArgumentException("Invalid verification code. Please check your authenticator app and try again."); // Get backup codes from verification result var resultData = JsonSerializer.Deserialize(JsonSerializer.Serialize(verificationResult.Data)); var backupCodes = resultData.GetProperty("backupCodes").EnumerateArray().Select(x => x.GetString()).ToArray(); // Create or update UserMfa record var existingMfa = await _dbContext.UserMfas.FirstOrDefaultAsync(m => m.UserId == userId); if (existingMfa != null) { // Update existing record existingMfa.SecretKey = secretKey; existingMfa.IsEnabled = true; existingMfa.IsVerified = true; existingMfa.VerifiedAt = DateTime.UtcNow; existingMfa.BackupCodes = JsonSerializer.Serialize(backupCodes); existingMfa.UpdatedAt = DateTime.UtcNow; } else { // Create new record var userMfa = new UserMfa { UserId = userId, SecretKey = secretKey, IsEnabled = true, IsVerified = true, VerifiedAt = DateTime.UtcNow, BackupCodes = JsonSerializer.Serialize(backupCodes), CreatedAt = DateTime.UtcNow }; _dbContext.UserMfas.Add(userMfa); } // Update user's MFA flag user.IsMfaEnabled = true; await _uow.CommitAsync(); return new { message = "2FA setup completed successfully", backupCodes = backupCodes, user = new { user.Id, user.FullName, user.UserName, user.Email, user.IsMfaEnabled } }; } catch { await _uow.RollbackAsync(); throw; } } public async Task VerifyTwoFA(object payload, HttpContext httpContext) { try { var data = DeserializePayload.Deserialize(payload); var userId = _userHelper.GetUserIdFromClaims(httpContext); var verificationCode = data.ContainsKey("verificationCode") ? data["verificationCode"].GetString() : null; if (string.IsNullOrWhiteSpace(verificationCode)) throw new ArgumentException("Verification code is required"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); if (!user.IsMfaEnabled) throw new InvalidOperationException("2FA is not enabled for this user"); var userMfa = await _dbContext.UserMfas.FirstOrDefaultAsync(m => m.UserId == userId && m.IsEnabled == true); if (userMfa == null || string.IsNullOrEmpty(userMfa.SecretKey)) throw new InvalidOperationException("2FA configuration not found"); // Verify using ThirdPartyService var verificationResult = await _thirdPartyService.VerifyTwoFACode(userMfa.SecretKey, verificationCode); if (verificationResult.StatusCode == 200) { // Update last used timestamp userMfa.LastUsedAt = DateTime.UtcNow; userMfa.UpdatedAt = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); } return verificationResult; } catch { throw; } } public async Task DisableTwoFA(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var userId = _userHelper.GetUserIdFromClaims(httpContext); var verificationCode = data.ContainsKey("verificationCode") ? data["verificationCode"].GetString() : null; if (string.IsNullOrWhiteSpace(verificationCode)) throw new ArgumentException("Verification code is required to disable 2FA"); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); if (!user.IsMfaEnabled) throw new InvalidOperationException("2FA is not enabled for this user"); var userMfa = await _dbContext.UserMfas.FirstOrDefaultAsync(m => m.UserId == userId && m.IsEnabled == true); if (userMfa == null || string.IsNullOrEmpty(userMfa.SecretKey)) throw new InvalidOperationException("2FA configuration not found"); // Verify using ThirdPartyService before disabling var disableResult = await _thirdPartyService.DisableTwoFA(userMfa.SecretKey, verificationCode); if (disableResult.StatusCode != 200) throw new ArgumentException("Invalid verification code. Cannot disable 2FA without valid code."); // Disable 2FA userMfa.IsEnabled = false; userMfa.UpdatedAt = DateTime.UtcNow; user.IsMfaEnabled = false; await _uow.CommitAsync(); return new { message = "2FA has been disabled successfully", user = new { user.Id, user.FullName, user.UserName, user.Email, user.IsMfaEnabled } }; } catch { await _uow.RollbackAsync(); throw; } } public async Task GetTwoFAStatus(HttpContext httpContext) { try { var userId = _userHelper.GetUserIdFromClaims(httpContext); var user = await _repository.GetUserByIdAsync(userId); if (user == null) throw new ArgumentException("User not found"); var userMfa = await _dbContext.UserMfas.FirstOrDefaultAsync(m => m.UserId == userId); return new { isMfaEnabled = user.IsMfaEnabled, isVerified = userMfa?.IsVerified ?? false, lastUsedAt = userMfa?.LastUsedAt, verifiedAt = userMfa?.VerifiedAt, hasBackupCodes = !string.IsNullOrEmpty(userMfa?.BackupCodes) }; } catch { throw; } } //------------------------------- HELPER FUNCTIONS ---------------------------------// public async Task CanRegisterUser(User userToCheck) { var existingUsers = await _repository.GetUserByIdentifiers(userToCheck.Email, userToCheck.MobileNumber, userToCheck.Nic, userToCheck.UserName, userToCheck.Optional1); if (!existingUsers.Any()) return true; foreach (var user in existingUsers) { if (user.UserTypeId == userToCheck.UserTypeId) return false; if (user.RoleId == userToCheck.RoleId) return false; if (user.IsLocked == false) return false; if (user.IsActive == false) return false; } return true; } private async Task LogAuthEvent(Guid? userId, string eventType, string? ipAddress, string? userAgent) { var authEvent = new AuthEventLog { UserId = userId, EventType = eventType, IPAddress = ipAddress, UserAgent = userAgent, CreatedAt = DateTime.UtcNow }; _dbContext.AuthEventLogs.Add(authEvent); await Task.CompletedTask; } private bool IsEmail(string identifier) { if (string.IsNullOrWhiteSpace(identifier)) return false; return System.Text.RegularExpressions.Regex.IsMatch( identifier, @"^[^@\s]+@[^@\s]+\.[^@\s]+$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); } private bool IsMobileNumber(string identifier) { if (string.IsNullOrWhiteSpace(identifier)) return false; var cleaned = identifier.Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "").Replace("+", ""); return System.Text.RegularExpressions.Regex.IsMatch(cleaned, @"^\d{7,15}$"); } }