1263 lines
41 KiB
C#
1263 lines
41 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 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",
|
|
$@"
|
|
<html>
|
|
<body style='font-family: Arial, sans-serif;'>
|
|
<h2>Your ERP System account has been created</h2>
|
|
<p>Username: <strong>{WebUtility.HtmlEncode(UserName ?? Email)}</strong></p>
|
|
<p>Temporary password: <strong>{{code}}</strong></p>
|
|
<p>Please log in and change your password as soon as possible.</p>
|
|
</body>
|
|
</html>");
|
|
}
|
|
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<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> 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<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}$");
|
|
}
|
|
|
|
}
|