UserBusinessImpl.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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 (fixed 111111 for test account abc@gmail.com)
  56. bool isTestAccount = request.email.ToLower() == "abc@gmail.com";
  57. string otpCode = isTestAccount ? "111111" : GenerateOtp();
  58. // Check if customer exists, if not create new
  59. var customer = dbContext.CustomerInfos
  60. .Where(c => c.Email == request.email)
  61. .FirstOrDefault();
  62. decimal? customerId = customer?.Id;
  63. if (customer == null)
  64. {
  65. // Create new customer record - manually get ID from Oracle sequence
  66. var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
  67. // Extract name from email (part before @)
  68. string emailUsername = request.email.Split('@')[0];
  69. var newCustomer = new CustomerInfo
  70. {
  71. Id = newCustomerId,
  72. Email = request.email,
  73. SurName = emailUsername,
  74. LastName = emailUsername,
  75. Status = true,
  76. IsVerified = false,
  77. CreatedDate = DateTime.Now,
  78. LastUpdate = DateTime.Now
  79. };
  80. dbContext.CustomerInfos.Add(newCustomer);
  81. await dbContext.SaveChangesAsync();
  82. customerId = newCustomerId;
  83. }
  84. // Check if there's an existing OTP record for this email (unused, not expired)
  85. int otpExpireMinutes = 5;
  86. var existingOtp = dbContext.OtpVerifications
  87. .Where(o => o.UserEmail == request.email && o.IsUsed == false)
  88. .OrderByDescending(o => o.CreatedDate)
  89. .FirstOrDefault();
  90. if (existingOtp != null)
  91. {
  92. // UPDATE existing record instead of creating new one
  93. existingOtp.OtpCode = otpCode;
  94. existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
  95. existingOtp.AttemptCount = 0; // Reset attempt count
  96. existingOtp.CustomerId = customerId; // Update customer ID if changed
  97. log.Info($"Updated existing OTP record ID={existingOtp.Id} for {request.email}");
  98. }
  99. else
  100. {
  101. // INSERT new record only if none exists
  102. var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
  103. var otpVerification = new OtpVerification
  104. {
  105. Id = otpId,
  106. CustomerId = customerId,
  107. UserEmail = request.email,
  108. OtpCode = otpCode,
  109. OtpType = 1, // Login OTP
  110. ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes),
  111. IsUsed = false,
  112. AttemptCount = 0,
  113. CreatedDate = DateTime.Now
  114. };
  115. dbContext.OtpVerifications.Add(otpVerification);
  116. log.Info($"Created new OTP record for {request.email}");
  117. }
  118. await dbContext.SaveChangesAsync();
  119. // Skip email sending for test account
  120. if (!isTestAccount)
  121. {
  122. // Add to MESSAGE_QUEUE for background email sending
  123. // Resolve template content now so Worker only needs to send email
  124. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  125. string templateCode = "OTP_LOGIN";
  126. // Query template and get language-specific content
  127. var template = dbContext.MessageTemplates
  128. .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status == true);
  129. if (template == null)
  130. {
  131. log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE");
  132. throw new Exception($"Email template '{templateCode}' not found");
  133. }
  134. // Get subject based on language (fallback to default column if _LO/_EN is null)
  135. string emailSubject = lang == "en"
  136. ? (template.SubjectEn ?? template.Subject ?? "")
  137. : (template.SubjectLo ?? template.Subject ?? "");
  138. // Get content based on language (fallback to default column if _LO/_EN is null)
  139. string emailContent = lang == "en"
  140. ? (template.ContentEn ?? template.Content ?? "")
  141. : (template.ContentLo ?? template.Content ?? "");
  142. // Replace placeholders in content
  143. emailContent = emailContent
  144. .Replace("{{OTP_CODE}}", otpCode)
  145. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  146. // Replace placeholders in subject (if any)
  147. emailSubject = emailSubject
  148. .Replace("{{OTP_CODE}}", otpCode)
  149. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  150. var emailMessageID = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
  151. var emailMessage = new MessageQueue
  152. {
  153. Id = emailMessageID,
  154. MessageType = 1, // Email
  155. Recipient = request.email,
  156. Subject = emailSubject, // Pre-resolved subject
  157. Content = emailContent, // Pre-resolved content
  158. Priority = true, // High priority
  159. Status = 0, // Pending
  160. ScheduledAt = DateTime.Now,
  161. RetryCount = 0,
  162. MaxRetry = 3,
  163. CreatedBy = customerId,
  164. CreatedDate = DateTime.Now
  165. };
  166. dbContext.MessageQueues.Add(emailMessage);
  167. await dbContext.SaveChangesAsync();
  168. }
  169. log.Info($"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
  170. //return DotnetLib.Http.HttpResponse.BuildResponse(
  171. // log,
  172. // url,
  173. // json,
  174. // CommonErrorCode.Success,
  175. // ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
  176. // new
  177. // {
  178. // email = request.email,
  179. // expireInSeconds = otpExpireMinutes * 60
  180. // }
  181. //);
  182. return DotnetLib.Http.HttpResponse.BuildResponse(
  183. log,
  184. url,
  185. json,
  186. CommonErrorCode.Success,
  187. ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
  188. new
  189. {
  190. email = request.email,
  191. expireInSeconds = otpExpireMinutes * 60
  192. }
  193. );
  194. }
  195. catch (Exception exception)
  196. {
  197. log.Error("Exception: ", exception);
  198. }
  199. return DotnetLib.Http.HttpResponse.BuildResponse(
  200. log,
  201. url,
  202. json,
  203. CommonErrorCode.SystemError,
  204. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  205. new { }
  206. );
  207. }
  208. /// <summary>
  209. /// Verify OTP and complete login - return JWT token
  210. /// </summary>
  211. public async Task<IActionResult> VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request)
  212. {
  213. var url = httpRequest.Path;
  214. var json = JsonConvert.SerializeObject(request);
  215. log.Debug("URL: " + url + " => Request: " + json);
  216. try
  217. {
  218. if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode))
  219. {
  220. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  221. return DotnetLib.Http.HttpResponse.BuildResponse(
  222. log,
  223. url,
  224. json,
  225. CommonErrorCode.RequiredFieldMissing,
  226. ConfigManager.Instance.GetConfigWebValue("EMAIL_OTP_REQUIRED", lang),
  227. new { }
  228. );
  229. }
  230. // Get language for response messages
  231. string responseLang = CommonLogic.GetLanguage(httpRequest, request.lang);
  232. // Find valid OTP
  233. var otpRecord = dbContext.OtpVerifications
  234. .Where(o => o.UserEmail == request.email
  235. && o.OtpCode == request.otpCode
  236. && o.IsUsed == false
  237. && o.ExpiredAt > DateTime.Now)
  238. .OrderByDescending(o => o.CreatedDate)
  239. .FirstOrDefault();
  240. if (otpRecord == null)
  241. {
  242. // Check if OTP exists but expired or used
  243. var anyOtp = dbContext.OtpVerifications
  244. .Where(o => o.UserEmail == request.email && o.OtpCode == request.otpCode)
  245. .FirstOrDefault();
  246. if (anyOtp != null)
  247. {
  248. if (anyOtp.IsUsed == true)
  249. {
  250. return DotnetLib.Http.HttpResponse.BuildResponse(
  251. log,
  252. url,
  253. json,
  254. CommonErrorCode.OtpAlreadyUsed,
  255. ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang),
  256. new { }
  257. );
  258. }
  259. if (anyOtp.ExpiredAt <= DateTime.Now)
  260. {
  261. return DotnetLib.Http.HttpResponse.BuildResponse(
  262. log,
  263. url,
  264. json,
  265. CommonErrorCode.OtpExpired,
  266. ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang),
  267. new { }
  268. );
  269. }
  270. }
  271. return DotnetLib.Http.HttpResponse.BuildResponse(
  272. log,
  273. url,
  274. json,
  275. CommonErrorCode.OtpInvalid,
  276. ConfigManager.Instance.GetConfigWebValue("OTP_INVALID", responseLang),
  277. new { }
  278. );
  279. }
  280. // Mark OTP as used
  281. otpRecord.IsUsed = true;
  282. // Get customer info
  283. var customer = dbContext.CustomerInfos
  284. .Where(c => c.Email == request.email)
  285. .FirstOrDefault();
  286. if (customer == null)
  287. {
  288. return DotnetLib.Http.HttpResponse.BuildResponse(
  289. log,
  290. url,
  291. json,
  292. CommonErrorCode.UserNotFound,
  293. ConfigManager.Instance.GetConfigWebValue("USER_NOT_FOUND", responseLang),
  294. new { }
  295. );
  296. }
  297. // Update customer verification status
  298. customer.IsVerified = true;
  299. customer.LastLoginDate = DateTime.Now;
  300. customer.LastUpdate = DateTime.Now;
  301. // Generate JWT tokens
  302. int tokenExpireHours = 24;
  303. int refreshTokenExpireDays = 30;
  304. string accessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
  305. string refreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
  306. var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
  307. var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
  308. // Revoke old tokens
  309. var oldTokens = dbContext.UserTokens
  310. .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
  311. .ToList();
  312. foreach (var oldToken in oldTokens)
  313. {
  314. oldToken.IsRevoked = true;
  315. }
  316. // Save new token
  317. var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
  318. var userToken = new UserToken
  319. {
  320. Id = tokenId,
  321. CustomerId = customer.Id,
  322. AccessToken = accessToken,
  323. RefreshToken = refreshToken,
  324. TokenType = "Bearer",
  325. DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
  326. IpAddress = GetClientIpAddress(httpRequest),
  327. ExpiredAt = expiresAt,
  328. RefreshExpiredAt = refreshExpiresAt,
  329. IsRevoked = false,
  330. CreatedDate = DateTime.Now,
  331. LastUsed = DateTime.Now
  332. };
  333. dbContext.UserTokens.Add(userToken);
  334. await dbContext.SaveChangesAsync();
  335. return DotnetLib.Http.HttpResponse.BuildResponse(
  336. log,
  337. url,
  338. json,
  339. CommonErrorCode.Success,
  340. ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", responseLang),
  341. new
  342. {
  343. userId = customer.Id,
  344. email = customer.Email ?? "",
  345. fullName = $"{customer.SurName} {customer.LastName}".Trim(),
  346. avatarUrl = customer.AvatarUrl,
  347. accessToken,
  348. refreshToken,
  349. expiresAt
  350. }
  351. );
  352. }
  353. catch (Exception exception)
  354. {
  355. log.Error("Exception: ", exception);
  356. }
  357. return DotnetLib.Http.HttpResponse.BuildResponse(
  358. log,
  359. url,
  360. json,
  361. CommonErrorCode.SystemError,
  362. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  363. new { }
  364. );
  365. }
  366. /// <summary>
  367. /// Generate 6-digit OTP code
  368. /// </summary>
  369. private string GenerateOtp()
  370. {
  371. using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
  372. {
  373. var bytes = new byte[4];
  374. rng.GetBytes(bytes);
  375. var number = Math.Abs(BitConverter.ToInt32(bytes, 0)) % 1000000;
  376. return number.ToString("D6");
  377. }
  378. }
  379. /// <summary>
  380. /// Get client IP address
  381. /// </summary>
  382. private string GetClientIpAddress(HttpRequest httpRequest)
  383. {
  384. var ipAddress = httpRequest.Headers["X-Forwarded-For"].FirstOrDefault();
  385. if (string.IsNullOrEmpty(ipAddress))
  386. {
  387. ipAddress = httpRequest.HttpContext.Connection.RemoteIpAddress?.ToString();
  388. }
  389. return ipAddress ?? "Unknown";
  390. }
  391. }
  392. }