UserBusinessImpl.cs 16 KB

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