UserBusinessImpl.cs 19 KB

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