UserBusinessImpl.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. using Common;
  2. using Common.Constant;
  3. using Common.Http;
  4. using Common.Logic;
  5. //using Esim.Apis.Logic;
  6. using Esim.Apis.Singleton;
  7. using Database;
  8. using Database.Database;
  9. using log4net;
  10. using Microsoft.AspNetCore.Mvc;
  11. using Microsoft.VisualBasic;
  12. using Newtonsoft.Json;
  13. using System;
  14. using System.Threading.Tasks;
  15. using System.Xml.Serialization;
  16. namespace Esim.Apis.Business
  17. {
  18. public class UserBusinessImpl : IUserBusiness
  19. {
  20. private static readonly log4net.ILog log = log4net.LogManager.GetLogger(
  21. typeof(UserBusinessImpl)
  22. );
  23. private ModelContext dbContext;
  24. IConfiguration configuration;
  25. public UserBusinessImpl(ModelContext _dbContext, IConfiguration _configuration)
  26. {
  27. dbContext = _dbContext;
  28. configuration = _configuration;
  29. }
  30. private string GetParameter(string key)
  31. {
  32. return configuration.GetSection(key).Value ?? "";
  33. }
  34. /// <summary>
  35. /// Request OTP to be sent to email
  36. /// </summary>
  37. public async Task<IActionResult> RequestOtp(HttpRequest httpRequest, RequestOtpReq request)
  38. {
  39. var url = httpRequest.Path;
  40. var json = JsonConvert.SerializeObject(request);
  41. log.Debug("URL: " + url + " => Request: " + json);
  42. try
  43. {
  44. if (string.IsNullOrEmpty(request.email))
  45. {
  46. return DotnetLib.Http.HttpResponse.BuildResponse(
  47. log,
  48. url,
  49. json,
  50. CommonErrorCode.RequiredFieldMissing,
  51. ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED"),
  52. new { }
  53. );
  54. }
  55. // Generate 6-digit OTP
  56. string otpCode = GenerateOtp();
  57. // Check if customer exists, if not create new
  58. var customer = dbContext.CustomerInfos
  59. .Where(c => c.Email == request.email)
  60. .FirstOrDefault();
  61. decimal? customerId = customer?.Id;
  62. if (customer == null)
  63. {
  64. // Create new customer record - manually get ID from Oracle sequence
  65. var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
  66. var newCustomer = new CustomerInfo
  67. {
  68. Id = newCustomerId,
  69. Email = request.email,
  70. Status = true,
  71. IsVerified = false,
  72. CreatedDate = DateTime.Now,
  73. LastUpdate = DateTime.Now
  74. };
  75. dbContext.CustomerInfos.Add(newCustomer);
  76. await dbContext.SaveChangesAsync();
  77. customerId = newCustomerId;
  78. }
  79. // Invalidate previous unused OTPs for this email
  80. var oldOtps = dbContext.OtpVerifications
  81. .Where(o => o.UserEmail == request.email && o.IsUsed == false)
  82. .ToList();
  83. foreach (var oldOtp in oldOtps)
  84. {
  85. oldOtp.IsUsed = true;
  86. }
  87. // Create new OTP record
  88. int otpExpireMinutes = 5;
  89. //var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
  90. var otpVerification = new OtpVerification
  91. {
  92. //Id = otpId,
  93. CustomerId = customerId,
  94. UserEmail = request.email,
  95. OtpCode = otpCode,
  96. OtpType = 1, // Login OTP
  97. ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes),
  98. IsUsed = false,
  99. AttemptCount = 0,
  100. CreatedDate = DateTime.Now
  101. };
  102. dbContext.OtpVerifications.Add(otpVerification);
  103. await dbContext.SaveChangesAsync();
  104. // Add to MESSAGE_QUEUE for background email sending
  105. // Resolve template content now so Worker only needs to send email
  106. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  107. string templateCode = "OTP_LOGIN";
  108. // Query template and get language-specific content
  109. var template = dbContext.MessageTemplates
  110. .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status == true);
  111. if (template == null)
  112. {
  113. log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE");
  114. throw new Exception($"Email template '{templateCode}' not found");
  115. }
  116. // Get subject based on language (fallback to default column if _LO/_EN is null)
  117. string emailSubject = lang == "en"
  118. ? (template.SubjectEn ?? template.Subject ?? "")
  119. : (template.SubjectLo ?? template.Subject ?? "");
  120. // Get content based on language (fallback to default column if _LO/_EN is null)
  121. string emailContent = lang == "en"
  122. ? (template.ContentEn ?? template.Content ?? "")
  123. : (template.ContentLo ?? template.Content ?? "");
  124. // Replace placeholders in content
  125. emailContent = emailContent
  126. .Replace("{{OTP_CODE}}", otpCode)
  127. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  128. // Replace placeholders in subject (if any)
  129. emailSubject = emailSubject
  130. .Replace("{{OTP_CODE}}", otpCode)
  131. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  132. var emailMessage = new MessageQueue
  133. {
  134. MessageType = 1, // Email
  135. Recipient = request.email,
  136. Subject = emailSubject, // Pre-resolved subject
  137. Content = emailContent, // Pre-resolved content
  138. Priority = true, // High priority
  139. Status = 0, // Pending
  140. ScheduledAt = DateTime.Now,
  141. RetryCount = 0,
  142. MaxRetry = 3,
  143. CreatedBy = customerId,
  144. CreatedDate = DateTime.Now
  145. };
  146. dbContext.MessageQueues.Add(emailMessage);
  147. await dbContext.SaveChangesAsync();
  148. log.Info($"OTP generated for {request.email}: {otpCode} - Email queued");
  149. return DotnetLib.Http.HttpResponse.BuildResponse(
  150. log,
  151. url,
  152. json,
  153. CommonErrorCode.Success,
  154. ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
  155. new
  156. {
  157. email = request.email,
  158. expireInSeconds = otpExpireMinutes * 60
  159. }
  160. );
  161. }
  162. catch (Exception exception)
  163. {
  164. log.Error("Exception: ", exception);
  165. }
  166. return DotnetLib.Http.HttpResponse.BuildResponse(
  167. log,
  168. url,
  169. json,
  170. CommonErrorCode.SystemError,
  171. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  172. new { }
  173. );
  174. }
  175. /// <summary>
  176. /// Verify OTP and complete login - return JWT token
  177. /// </summary>
  178. public async Task<IActionResult> VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request)
  179. {
  180. var url = httpRequest.Path;
  181. var json = JsonConvert.SerializeObject(request);
  182. log.Debug("URL: " + url + " => Request: " + json);
  183. try
  184. {
  185. if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode))
  186. {
  187. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  188. return DotnetLib.Http.HttpResponse.BuildResponse(
  189. log,
  190. url,
  191. json,
  192. CommonErrorCode.RequiredFieldMissing,
  193. ConfigManager.Instance.GetConfigWebValue("EMAIL_OTP_REQUIRED", lang),
  194. new { }
  195. );
  196. }
  197. // Get language for response messages
  198. string responseLang = CommonLogic.GetLanguage(httpRequest, request.lang);
  199. // Find valid OTP
  200. var otpRecord = dbContext.OtpVerifications
  201. .Where(o => o.UserEmail == request.email
  202. && o.OtpCode == request.otpCode
  203. && o.IsUsed == false
  204. && o.ExpiredAt > DateTime.Now)
  205. .OrderByDescending(o => o.CreatedDate)
  206. .FirstOrDefault();
  207. if (otpRecord == null)
  208. {
  209. // Check if OTP exists but expired or used
  210. var anyOtp = dbContext.OtpVerifications
  211. .Where(o => o.UserEmail == request.email && o.OtpCode == request.otpCode)
  212. .FirstOrDefault();
  213. if (anyOtp != null)
  214. {
  215. if (anyOtp.IsUsed == true)
  216. {
  217. return DotnetLib.Http.HttpResponse.BuildResponse(
  218. log,
  219. url,
  220. json,
  221. CommonErrorCode.OtpAlreadyUsed,
  222. ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang),
  223. new { }
  224. );
  225. }
  226. if (anyOtp.ExpiredAt <= DateTime.Now)
  227. {
  228. return DotnetLib.Http.HttpResponse.BuildResponse(
  229. log,
  230. url,
  231. json,
  232. CommonErrorCode.OtpExpired,
  233. ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang),
  234. new { }
  235. );
  236. }
  237. }
  238. return DotnetLib.Http.HttpResponse.BuildResponse(
  239. log,
  240. url,
  241. json,
  242. CommonErrorCode.OtpInvalid,
  243. ConfigManager.Instance.GetConfigWebValue("OTP_INVALID", responseLang),
  244. new { }
  245. );
  246. }
  247. // Mark OTP as used
  248. otpRecord.IsUsed = true;
  249. // Get customer info
  250. var customer = dbContext.CustomerInfos
  251. .Where(c => c.Email == request.email)
  252. .FirstOrDefault();
  253. if (customer == null)
  254. {
  255. return DotnetLib.Http.HttpResponse.BuildResponse(
  256. log,
  257. url,
  258. json,
  259. CommonErrorCode.UserNotFound,
  260. ConfigManager.Instance.GetConfigWebValue("USER_NOT_FOUND", responseLang),
  261. new { }
  262. );
  263. }
  264. // Update customer verification status
  265. customer.IsVerified = true;
  266. customer.LastLoginDate = DateTime.Now;
  267. customer.LastUpdate = DateTime.Now;
  268. // Generate JWT tokens
  269. int tokenExpireHours = 24;
  270. int refreshTokenExpireDays = 30;
  271. string accessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
  272. string refreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
  273. var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
  274. var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
  275. // Revoke old tokens
  276. var oldTokens = dbContext.UserTokens
  277. .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
  278. .ToList();
  279. foreach (var oldToken in oldTokens)
  280. {
  281. oldToken.IsRevoked = true;
  282. }
  283. // Save new token
  284. var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
  285. var userToken = new UserToken
  286. {
  287. Id = tokenId,
  288. CustomerId = customer.Id,
  289. AccessToken = accessToken,
  290. RefreshToken = refreshToken,
  291. TokenType = "Bearer",
  292. DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
  293. IpAddress = GetClientIpAddress(httpRequest),
  294. ExpiredAt = expiresAt,
  295. RefreshExpiredAt = refreshExpiresAt,
  296. IsRevoked = false,
  297. CreatedDate = DateTime.Now,
  298. LastUsed = DateTime.Now
  299. };
  300. dbContext.UserTokens.Add(userToken);
  301. await dbContext.SaveChangesAsync();
  302. return DotnetLib.Http.HttpResponse.BuildResponse(
  303. log,
  304. url,
  305. json,
  306. CommonErrorCode.Success,
  307. ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", responseLang),
  308. new
  309. {
  310. userId = customer.Id,
  311. email = customer.Email ?? "",
  312. fullName = $"{customer.SurName} {customer.LastName}".Trim(),
  313. avatarUrl = customer.AvatarUrl,
  314. accessToken,
  315. refreshToken,
  316. expiresAt
  317. }
  318. );
  319. }
  320. catch (Exception exception)
  321. {
  322. log.Error("Exception: ", exception);
  323. }
  324. return DotnetLib.Http.HttpResponse.BuildResponse(
  325. log,
  326. url,
  327. json,
  328. CommonErrorCode.SystemError,
  329. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  330. new { }
  331. );
  332. }
  333. /// <summary>
  334. /// Generate 6-digit OTP code
  335. /// </summary>
  336. private string GenerateOtp()
  337. {
  338. using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
  339. {
  340. var bytes = new byte[4];
  341. rng.GetBytes(bytes);
  342. var number = Math.Abs(BitConverter.ToInt32(bytes, 0)) % 1000000;
  343. return number.ToString("D6");
  344. }
  345. }
  346. /// <summary>
  347. /// Get client IP address
  348. /// </summary>
  349. private string GetClientIpAddress(HttpRequest httpRequest)
  350. {
  351. var ipAddress = httpRequest.Headers["X-Forwarded-For"].FirstOrDefault();
  352. if (string.IsNullOrEmpty(ipAddress))
  353. {
  354. ipAddress = httpRequest.HttpContext.Connection.RemoteIpAddress?.ToString();
  355. }
  356. return ipAddress ?? "Unknown";
  357. }
  358. }
  359. }