UserBusinessImpl.cs 16 KB

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