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 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 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", $@"

Verification OTP

Your OTP code is:

{otpCode}

This code expires in 10 minutes.

Reference Number: {recovery.RecoveryReferenceNum}

" ); } 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", $@"

Login OTP

Hello {user.FullName ?? user.UserName},

Your OTP code is:

{otpCode}

This code expires in 10 minutes.

Reference Number: {recovery.RecoveryReferenceNum}

" ); } 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 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 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; } }