using Common; using Common.Constant; using Common.Http; using Common.Logic; //using Esim.Apis.Logic; using Esim.Apis.Singleton; using Database; using Database.Database; using log4net; using Microsoft.AspNetCore.Mvc; using Microsoft.VisualBasic; using Newtonsoft.Json; using System; using System.Threading.Tasks; using System.Xml.Serialization; namespace Esim.Apis.Business { public class UserBusinessImpl : IUserBusiness { private static readonly log4net.ILog log = log4net.LogManager.GetLogger( typeof(UserBusinessImpl) ); private ModelContext dbContext; IConfiguration configuration; public UserBusinessImpl(ModelContext _dbContext, IConfiguration _configuration) { dbContext = _dbContext; configuration = _configuration; } private string GetParameter(string key) { return configuration.GetSection(key).Value ?? ""; } /// /// Request OTP to be sent to email /// public async Task RequestOtp(HttpRequest httpRequest, RequestOtpReq request) { var url = httpRequest.Path; var json = JsonConvert.SerializeObject(request); log.Debug("URL: " + url + " => Request: " + json); try { if (string.IsNullOrEmpty(request.email)) { return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.RequiredFieldMissing, ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED"), new { } ); } // Generate 6-digit OTP string otpCode = GenerateOtp(); // Check if customer exists, if not create new var customer = dbContext.CustomerInfos .Where(c => c.Email == request.email) .FirstOrDefault(); decimal? customerId = customer?.Id; if (customer == null) { // Create new customer record - manually get ID from Oracle sequence var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ"); var newCustomer = new CustomerInfo { Id = newCustomerId, Email = request.email, Status = true, IsVerified = false, CreatedDate = DateTime.Now, LastUpdate = DateTime.Now }; dbContext.CustomerInfos.Add(newCustomer); await dbContext.SaveChangesAsync(); customerId = newCustomerId; } // Invalidate previous unused OTPs for this email var oldOtps = dbContext.OtpVerifications .Where(o => o.UserEmail == request.email && o.IsUsed == false) .ToList(); foreach (var oldOtp in oldOtps) { oldOtp.IsUsed = true; } // Create new OTP record int otpExpireMinutes = 5; //var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ"); var otpVerification = new OtpVerification { //Id = otpId, CustomerId = customerId, UserEmail = request.email, OtpCode = otpCode, OtpType = 1, // Login OTP ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes), IsUsed = false, AttemptCount = 0, CreatedDate = DateTime.Now }; dbContext.OtpVerifications.Add(otpVerification); await dbContext.SaveChangesAsync(); // Add to MESSAGE_QUEUE for background email sending // Resolve template content now so Worker only needs to send email string lang = (request.lang ?? "lo").ToLower(); string templateCode = "OTP_LOGIN"; // Query template and get language-specific content var template = dbContext.MessageTemplates .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status == true); if (template == null) { log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE"); throw new Exception($"Email template '{templateCode}' not found"); } // Get subject based on language (fallback to default column if _LO/_EN is null) string emailSubject = lang == "en" ? (template.SubjectEn ?? template.Subject ?? "") : (template.SubjectLo ?? template.Subject ?? ""); // Get content based on language (fallback to default column if _LO/_EN is null) string emailContent = lang == "en" ? (template.ContentEn ?? template.Content ?? "") : (template.ContentLo ?? template.Content ?? ""); // Replace placeholders in content emailContent = emailContent .Replace("{{OTP_CODE}}", otpCode) .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString()); // Replace placeholders in subject (if any) emailSubject = emailSubject .Replace("{{OTP_CODE}}", otpCode) .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString()); var emailMessage = new MessageQueue { MessageType = 1, // Email Recipient = request.email, Subject = emailSubject, // Pre-resolved subject Content = emailContent, // Pre-resolved content Priority = true, // High priority Status = 0, // Pending ScheduledAt = DateTime.Now, RetryCount = 0, MaxRetry = 3, CreatedBy = customerId, CreatedDate = DateTime.Now }; dbContext.MessageQueues.Add(emailMessage); await dbContext.SaveChangesAsync(); log.Info($"OTP generated for {request.email}: {otpCode} - Email queued"); return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.Success, ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"), new { email = request.email, expireInSeconds = otpExpireMinutes * 60 } ); } catch (Exception exception) { log.Error("Exception: ", exception); } return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.SystemError, ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"), new { } ); } /// /// Verify OTP and complete login - return JWT token /// public async Task VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request) { var url = httpRequest.Path; var json = JsonConvert.SerializeObject(request); log.Debug("URL: " + url + " => Request: " + json); try { if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode)) { string lang = (request.lang ?? "lo").ToLower(); return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.RequiredFieldMissing, ConfigManager.Instance.GetConfigWebValue("EMAIL_OTP_REQUIRED", lang), new { } ); } // Get language for response messages string responseLang = (request.lang ?? "lo").ToLower(); // Find valid OTP var otpRecord = dbContext.OtpVerifications .Where(o => o.UserEmail == request.email && o.OtpCode == request.otpCode && o.IsUsed == false && o.ExpiredAt > DateTime.Now) .OrderByDescending(o => o.CreatedDate) .FirstOrDefault(); if (otpRecord == null) { // Check if OTP exists but expired or used var anyOtp = dbContext.OtpVerifications .Where(o => o.UserEmail == request.email && o.OtpCode == request.otpCode) .FirstOrDefault(); if (anyOtp != null) { if (anyOtp.IsUsed == true) { return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.OtpAlreadyUsed, ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang), new { } ); } if (anyOtp.ExpiredAt <= DateTime.Now) { return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.OtpExpired, ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang), new { } ); } } return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.OtpInvalid, ConfigManager.Instance.GetConfigWebValue("OTP_INVALID", responseLang), new { } ); } // Mark OTP as used otpRecord.IsUsed = true; // Get customer info var customer = dbContext.CustomerInfos .Where(c => c.Email == request.email) .FirstOrDefault(); if (customer == null) { return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.UserNotFound, ConfigManager.Instance.GetConfigWebValue("USER_NOT_FOUND", responseLang), new { } ); } // Update customer verification status customer.IsVerified = true; customer.LastLoginDate = DateTime.Now; customer.LastUpdate = DateTime.Now; // Generate JWT tokens int tokenExpireHours = 24; int refreshTokenExpireDays = 30; string accessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? ""); string refreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? ""); var expiresAt = DateTime.Now.AddHours(tokenExpireHours); var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays); // Revoke old tokens var oldTokens = dbContext.UserTokens .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false) .ToList(); foreach (var oldToken in oldTokens) { oldToken.IsRevoked = true; } // Save new token var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ"); var userToken = new UserToken { Id = tokenId, CustomerId = customer.Id, AccessToken = accessToken, RefreshToken = refreshToken, TokenType = "Bearer", DeviceInfo = httpRequest.Headers["User-Agent"].ToString(), IpAddress = GetClientIpAddress(httpRequest), ExpiredAt = expiresAt, RefreshExpiredAt = refreshExpiresAt, IsRevoked = false, CreatedDate = DateTime.Now, LastUsed = DateTime.Now }; dbContext.UserTokens.Add(userToken); await dbContext.SaveChangesAsync(); return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.Success, ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", responseLang), new { userId = customer.Id, email = customer.Email ?? "", fullName = $"{customer.SurName} {customer.LastName}".Trim(), avatarUrl = customer.AvatarUrl, accessToken, refreshToken, expiresAt } ); } catch (Exception exception) { log.Error("Exception: ", exception); } return DotnetLib.Http.HttpResponse.BuildResponse( log, url, json, CommonErrorCode.SystemError, ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"), new { } ); } /// /// Generate 6-digit OTP code /// private string GenerateOtp() { using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create()) { var bytes = new byte[4]; rng.GetBytes(bytes); var number = Math.Abs(BitConverter.ToInt32(bytes, 0)) % 1000000; return number.ToString("D6"); } } /// /// Get client IP address /// private string GetClientIpAddress(HttpRequest httpRequest) { var ipAddress = httpRequest.Headers["X-Forwarded-For"].FirstOrDefault(); if (string.IsNullOrEmpty(ipAddress)) { ipAddress = httpRequest.HttpContext.Connection.RemoteIpAddress?.ToString(); } return ipAddress ?? "Unknown"; } #endregion } }