This commit is contained in:
Dhananjaya99
2026-07-08 11:11:50 +05:30
parent 2cd2741f77
commit de7b40a147
51 changed files with 6839 additions and 0 deletions
@@ -0,0 +1,496 @@
using AuthHex.Infra.UoW;
using AuthHex.Interfaces;
using AuthHex.Models;
using AuthHex.Models.IOs;
using AuthHex.Utility;
using AuthHex.Utility.passwordHasher;
using AuthSystem.API.Utils;
using Core_Archi.Helpers;
using System.Security.Cryptography;
namespace AuthHex.Services.AltOptionManager;
public class AltOptionManagerService
{
private readonly IRecoveryManagerRepository _repository;
private readonly IUserManageRepository _userRepository;
private readonly IAltOptionManagerRepository _altOptionRepository;
private readonly IUnitOfWork _uow;
private readonly JwtTokenHelper _jwt;
private readonly AppDBContext _dbContext;
private readonly ThirdPartyService _thirdPartyService;
private readonly IConfiguration _configuration;
private readonly UserHelper _userHelper;
public AltOptionManagerService(
IRecoveryManagerRepository Recoveryrepository,
IUserManageRepository userRepository,
IAltOptionManagerRepository altOptionRepository,
IUnitOfWork uow,
JwtTokenHelper jwt,
AppDBContext dbContext,
ThirdPartyService thirdPartyService,
IConfiguration configuration,
UserHelper userHelper)
{
_repository = Recoveryrepository;
_userRepository = userRepository;
_altOptionRepository = altOptionRepository;
_uow = uow;
_jwt = jwt;
_dbContext = dbContext;
_thirdPartyService = thirdPartyService;
_configuration = configuration;
_userHelper = userHelper;
}
public async Task<object> IsAvailable(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var Identifier = data.ContainsKey("Identifier") ? data["Identifier"].GetString() : null;
var Recovery = data.ContainsKey("Recovery") ? data["Recovery"].GetString() : null;
var existingUsers = await _userRepository.GetUserByIdentifiers(
email: Identifier,
mobileNumber: Identifier,
nic: Identifier,
username: Identifier,
Optional1: Identifier
);
if (existingUsers == null || !existingUsers.Any())
{
return new { IsAvailable = true, Message = "Identifier is available" };
}
if (Recovery == null)
{
return new { IsAvailable = false, Message = "Identifier already in use" };
}
return new { existingUsers };
}
public async Task<object> SendOtp(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var identifier = data.ContainsKey("identifier") ? data["identifier"].GetString() : null;
var numberOfDigits = data.ContainsKey("numberOfDigits") ? data["numberOfDigits"].GetInt32() : 6;
var newUser = data.ContainsKey("newUser") ? data["newUser"].GetBoolean() : false;
if (string.IsNullOrWhiteSpace(identifier))
throw new ArgumentException("Identifier is required.");
var otpCode = await GenerateOtpCode(numberOfDigits);
Recovery recovery;
if (newUser)
{
recovery = new Recovery
{
RecoveryReferenceNum = GenerateSecureReferenceNumber(),
OTP = otpCode,
Status = "Pending",
IsUsed = false,
CreatedAt = DateTime.UtcNow,
ExpirationTime = DateTime.UtcNow.AddMinutes(10),
RecoveryType = "OTP FOR NEW USER"
};
await _repository.AddRecoveryAsync(recovery);
await _uow.CommitAsync();
if (!string.IsNullOrWhiteSpace(identifier))
{
if (IsEmail(identifier))
{
await _thirdPartyService.EmailConfiguration(
identifier,
otpCode,
"Verification OTP",
$@"
<h2>Verification OTP</h2>
<p>Your OTP code is:</p>
<h1>{otpCode}</h1>
<p>This code expires in 10 minutes.</p>
<p><strong>Reference Number:</strong> {recovery.RecoveryReferenceNum}</p>
"
);
}
else if (IsMobileNumber(identifier))
{
await _thirdPartyService.SendSmsAsync(
identifier,
otpCode,
$"Your OTP is {otpCode}. It expires in 10 minutes. Ref: {recovery.RecoveryReferenceNum}"
);
}
else
{
throw new ArgumentException("Invalid identifier format. Must be a valid email or mobile number.");
}
}
return new
{
Success = true,
Message = "OTP sent successfully for new user.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = recovery.RecoveryType
}
};
}
//exisiting one
var user = await _userRepository.GetUserByIdentifierAndType(identifier, null);
if (user == null)
throw new KeyNotFoundException("No user found with the provided identifier.");
if (user.IsLocked == true)
throw new InvalidOperationException("User account is locked. Please contact support.");
if (user.IsActive == false)
throw new InvalidOperationException("User account is inactive. Please contact support.");
recovery = new Recovery
{
UserId = user.Id,
RecoveryReferenceNum = GenerateSecureReferenceNumber(),
OTP = otpCode,
Status = "Pending",
IsUsed = false,
CreatedAt = DateTime.UtcNow,
ExpirationTime = DateTime.UtcNow.AddMinutes(10),
RecoveryType = "OTP FOR LOGIN"
};
await _repository.AddRecoveryAsync(recovery);
await _uow.CommitAsync();
if (!string.IsNullOrWhiteSpace(user.MobileNumber))
{
await _thirdPartyService.SendSmsAsync(
user.MobileNumber,
otpCode,
$"Your login OTP is {otpCode}. It expires in 10 minutes. Ref: {recovery.RecoveryReferenceNum}"
);
}
if (!string.IsNullOrWhiteSpace(user.Email))
{
await _thirdPartyService.EmailConfiguration(
user.Email,
otpCode,
"Login OTP",
$@"
<h2>Login OTP</h2>
<p>Hello {user.FullName ?? user.UserName},</p>
<p>Your OTP code is:</p>
<h1>{otpCode}</h1>
<p>This code expires in 10 minutes.</p>
<p><strong>Reference Number:</strong> {recovery.RecoveryReferenceNum}</p>
"
);
}
return new
{
Success = true,
Message = "OTP sent successfully.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = recovery.RecoveryType
}
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> VerifyOTP(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);
Guid? userId = null;
if (data.ContainsKey("userId") && Guid.TryParse(data["userId"].GetString(), out var parsedUserId))
{
userId = parsedUserId;
}
var referenceNumber = data.ContainsKey("referenceNumber") ? data["referenceNumber"].GetString() : null;
var otpCode = data.ContainsKey("otpCode") ? data["otpCode"].GetString() : null;
var newUser = data.ContainsKey("newUser") ? data["newUser"].GetBoolean() : false;
var identifier = data.ContainsKey("identifier") ? data["identifier"].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 _repository.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 _repository.UpdateRecoveryAsync(recovery);
await _uow.CommitAsync();
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 _repository.UpdateRecoveryAsync(recovery);
User? user = null;
if (!string.IsNullOrWhiteSpace(identifier))
{
user = await _userRepository.GetUserByIdentifierAndType(identifier, null);
}
if (user is null && userId.HasValue)
{
user = await _userRepository.GetUserByIdAsync(userId.Value);
}
if (user is null && recovery.UserId.HasValue)
{
user = await _userRepository.GetUserByIdAsync(recovery.UserId.Value);
}
if (user is null)
throw new KeyNotFoundException("No user found for OTP verification.");
if (!string.IsNullOrWhiteSpace(identifier))
{
if (IsEmail(identifier))
{
user.Email = identifier;
user.EmailVerified = true;
}
else if (IsMobileNumber(identifier))
{
user.MobileNumber = identifier;
user.MobileNumberVerified = true;
}
else
{
user.Optional1 = identifier;
user.Optional1Verified = true;
}
}
await _userRepository.UpdateUserAsync(user);
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 = newUser
? "OTP verified successfully. Session started."
: "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;
}
}
private async Task<string> GenerateOtpCode(int numberOfDigits)
{
if (numberOfDigits <= 0)
throw new ArgumentException("Number of digits must be at least 1", nameof(numberOfDigits));
using var rng = RandomNumberGenerator.Create();
var otpCode = "";
for (int i = 0; i < numberOfDigits; i++)
{
byte[] randomBytes = new byte[4];
rng.GetBytes(randomBytes);
uint randomValue = BitConverter.ToUInt32(randomBytes, 0);
otpCode += (randomValue % 10).ToString();
}
return await Task.FromResult(otpCode);
}
private string GenerateSecureReferenceNumber()
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
using var rng = RandomNumberGenerator.Create();
byte[] randomBytes = new byte[8];
rng.GetBytes(randomBytes);
var randomPart = Convert.ToBase64String(randomBytes)
.Replace("/", "")
.Replace("+", "")
.Replace("=", "")
.Substring(0, 8).ToUpper();
return $"REC{timestamp}{randomPart}";
}
private bool IsEmail(string identifier)
{
if (string.IsNullOrWhiteSpace(identifier))
return false;
try
{
var emailRegex = new System.Text.RegularExpressions.Regex(
@"^[^@\s]+@[^@\s]+\.[^@\s]+$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return emailRegex.IsMatch(identifier);
}
catch
{
return false;
}
}
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}$");
}
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;
}
}