Files
ERP_Auth_Service/Services/UserManager/UserManagerService.cs
T
Dhananjaya99 de7b40a147 ini
2026-07-08 11:11:50 +05:30

1226 lines
40 KiB
C#

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<object> 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 PasswordHash = data.ContainsKey("password") ? PasswordHasher.Hash(data["password"].GetString()!) : null;
// 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();
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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<JsonElement>(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<object> 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<object> 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<object> 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<bool> 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}$");
}
}