UserBusinessImpl.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  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. using System.Net.Http;
  17. using System.Net.Http.Headers;
  18. using Newtonsoft.Json.Linq;
  19. using System.Linq;
  20. namespace Esim.Apis.Business
  21. {
  22. public class UserBusinessImpl : IUserBusiness
  23. {
  24. private static readonly log4net.ILog log = log4net.LogManager.GetLogger(
  25. typeof(UserBusinessImpl)
  26. );
  27. private ModelContext dbContext;
  28. IConfiguration configuration;
  29. public UserBusinessImpl(ModelContext _dbContext, IConfiguration _configuration)
  30. {
  31. dbContext = _dbContext;
  32. configuration = _configuration;
  33. }
  34. private string GetParameter(string key)
  35. {
  36. return configuration.GetSection(key).Value ?? "";
  37. }
  38. /// <summary>
  39. /// Request OTP to be sent to email
  40. /// </summary>
  41. public async Task<IActionResult> RequestOtp(HttpRequest httpRequest, RequestOtpReq request)
  42. {
  43. var url = httpRequest.Path;
  44. var json = JsonConvert.SerializeObject(request);
  45. log.Debug("URL: " + url + " => Request: " + json);
  46. try
  47. {
  48. if (string.IsNullOrEmpty(request.email))
  49. {
  50. return DotnetLib.Http.HttpResponse.BuildResponse(
  51. log,
  52. url,
  53. json,
  54. CommonErrorCode.RequiredFieldMissing,
  55. ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED"),
  56. new { }
  57. );
  58. }
  59. // Generate 6-digit OTP (fixed 111111 for test account abc@gmail.com)
  60. bool isTestAccount = request.email.ToLower() == "abc@gmail.com";
  61. string otpCode = isTestAccount ? "111111" : GenerateOtp();
  62. // Check if customer exists, if not create new
  63. var customer = dbContext.CustomerInfos
  64. .Where(c => c.Email == request.email)
  65. .FirstOrDefault();
  66. decimal? customerId = customer?.Id;
  67. if (customer == null)
  68. {
  69. // Create new customer record - manually get ID from Oracle sequence
  70. var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
  71. // Extract name from email (part before @)
  72. string emailUsername = request.email.Split('@')[0];
  73. var newCustomer = new CustomerInfo
  74. {
  75. Id = newCustomerId,
  76. Email = request.email,
  77. SurName = emailUsername,
  78. LastName = emailUsername,
  79. Status = true,
  80. IsVerified = false,
  81. CreatedDate = DateTime.Now,
  82. LastUpdate = DateTime.Now
  83. };
  84. dbContext.CustomerInfos.Add(newCustomer);
  85. await dbContext.SaveChangesAsync();
  86. customerId = newCustomerId;
  87. }
  88. // Check if there's an existing OTP record for this email (ANY record, used or unused)
  89. int otpExpireMinutes = 5;
  90. int minSecondsBetweenRequests = 60; // Anti-spam: minimum 60 seconds between requests
  91. var existingOtp = dbContext.OtpVerifications
  92. .Where(o => o.UserEmail == request.email)
  93. .OrderByDescending(o => o.CreatedDate)
  94. .FirstOrDefault();
  95. if (existingOtp != null)
  96. {
  97. // Anti-spam check: prevent rapid OTP requests
  98. var secondsSinceLastRequest = (DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)).TotalSeconds;
  99. if (secondsSinceLastRequest < minSecondsBetweenRequests)
  100. {
  101. var waitSeconds = (int)(minSecondsBetweenRequests - secondsSinceLastRequest);
  102. log.Warn($"Spam prevention: OTP request too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago.");
  103. return DotnetLib.Http.HttpResponse.BuildResponse(
  104. log,
  105. url,
  106. json,
  107. CommonErrorCode.OtpTooManyRequests,
  108. $"Please wait {waitSeconds} seconds before requesting a new OTP.",
  109. new {
  110. waitSeconds = waitSeconds,
  111. canRequestAt = existingOtp.CreatedDate?.AddSeconds(minSecondsBetweenRequests)
  112. }
  113. );
  114. }
  115. // UPDATE existing record - reuse for this email to prevent table bloat
  116. existingOtp.OtpCode = otpCode;
  117. existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
  118. existingOtp.AttemptCount = 0; // Reset attempt count
  119. existingOtp.IsUsed = false; // Reset to unused (allow reuse)
  120. existingOtp.CustomerId = customerId; // Update customer ID if changed
  121. existingOtp.CreatedDate = DateTime.Now; // Update to current time
  122. log.Info($"Updated existing OTP record ID={existingOtp.Id} for {request.email} (was {(existingOtp.IsUsed == true ? "used" : "unused")})");
  123. }
  124. else
  125. {
  126. // INSERT new record only for first-time email
  127. var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
  128. var otpVerification = new OtpVerification
  129. {
  130. Id = otpId,
  131. CustomerId = customerId,
  132. UserEmail = request.email,
  133. OtpCode = otpCode,
  134. OtpType = 1, // Login OTP
  135. ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes),
  136. IsUsed = false,
  137. AttemptCount = 0,
  138. CreatedDate = DateTime.Now
  139. };
  140. dbContext.OtpVerifications.Add(otpVerification);
  141. log.Info($"Created new OTP record for {request.email} (first time)");
  142. }
  143. await dbContext.SaveChangesAsync();
  144. // Skip email sending for test account
  145. if (!isTestAccount)
  146. {
  147. // Add to MESSAGE_QUEUE for background email sending
  148. // Resolve template content now so Worker only needs to send email
  149. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  150. string templateCode = "OTP_LOGIN";
  151. // Query template and get language-specific content
  152. var template = dbContext.MessageTemplates
  153. .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value);
  154. if (template == null)
  155. {
  156. log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE");
  157. throw new Exception($"Email template '{templateCode}' not found");
  158. }
  159. // Get subject based on language: vi=default, en=_EN, lo=_LO (default)
  160. string emailSubject = lang == "en" ? (template.SubjectEn ?? template.Subject ?? "")
  161. : lang == "vi" ? (template.Subject ?? "")
  162. : (template.SubjectLo ?? template.Subject ?? "");
  163. // Get content based on language: vi=default, en=_EN, lo=_LO (default)
  164. string emailContent = lang == "en" ? (template.ContentEn ?? template.Content ?? "")
  165. : lang == "vi" ? (template.Content ?? "")
  166. : (template.ContentLo ?? template.Content ?? "");
  167. // Replace placeholders in content
  168. emailContent = emailContent
  169. .Replace("{{OTP_CODE}}", otpCode)
  170. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  171. // Replace placeholders in subject (if any)
  172. emailSubject = emailSubject
  173. .Replace("{{OTP_CODE}}", otpCode)
  174. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  175. var emailMessageID = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
  176. var emailMessage = new MessageQueue
  177. {
  178. Id = emailMessageID,
  179. MessageType = 1, // Email
  180. Recipient = request.email,
  181. Subject = emailSubject, // Pre-resolved subject
  182. Content = emailContent, // Pre-resolved content
  183. Priority = true, // High priority
  184. Status = 0, // Pending
  185. ScheduledAt = DateTime.Now,
  186. RetryCount = 0,
  187. MaxRetry = 3,
  188. CreatedBy = customerId,
  189. CreatedDate = DateTime.Now
  190. };
  191. dbContext.MessageQueues.Add(emailMessage);
  192. await dbContext.SaveChangesAsync();
  193. }
  194. log.Info($"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
  195. //return DotnetLib.Http.HttpResponse.BuildResponse(
  196. // log,
  197. // url,
  198. // json,
  199. // CommonErrorCode.Success,
  200. // ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
  201. // new
  202. // {
  203. // email = request.email,
  204. // expireInSeconds = otpExpireMinutes * 60
  205. // }
  206. //);
  207. return DotnetLib.Http.HttpResponse.BuildResponse(
  208. log,
  209. url,
  210. json,
  211. CommonErrorCode.Success,
  212. ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
  213. new
  214. {
  215. email = request.email,
  216. expireInSeconds = otpExpireMinutes * 60
  217. }
  218. );
  219. }
  220. catch (Exception exception)
  221. {
  222. log.Error("Exception: ", exception);
  223. }
  224. return DotnetLib.Http.HttpResponse.BuildResponse(
  225. log,
  226. url,
  227. json,
  228. CommonErrorCode.SystemError,
  229. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  230. new { }
  231. );
  232. }
  233. /// <summary>
  234. /// Resend OTP - Only works if user has already requested OTP before
  235. /// Has reduced cooldown (30 seconds vs 60 seconds for RequestOtp)
  236. /// </summary>
  237. public async Task<IActionResult> ResendOtp(HttpRequest httpRequest, RequestOtpReq request)
  238. {
  239. var url = httpRequest.Path;
  240. var json = JsonConvert.SerializeObject(request);
  241. log.Debug("URL: " + url + " => ResendOtp Request: " + json);
  242. try
  243. {
  244. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  245. // Validate email is required
  246. if (string.IsNullOrEmpty(request.email))
  247. {
  248. return DotnetLib.Http.HttpResponse.BuildResponse(
  249. log,
  250. url,
  251. json,
  252. CommonErrorCode.RequiredFieldMissing,
  253. ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED", lang),
  254. new { }
  255. );
  256. }
  257. // Check if there's an existing OTP record for this email
  258. var existingOtp = dbContext.OtpVerifications
  259. .Where(o => o.UserEmail == request.email)
  260. .OrderByDescending(o => o.CreatedDate)
  261. .FirstOrDefault();
  262. // RESEND requires existing OTP - must have requested OTP before
  263. if (existingOtp == null)
  264. {
  265. log.Warn($"ResendOtp failed: No existing OTP record for {request.email}");
  266. return DotnetLib.Http.HttpResponse.BuildResponse(
  267. log,
  268. url,
  269. json,
  270. CommonErrorCode.OtpNotRequested,
  271. ConfigManager.Instance.GetConfigWebValue("OTP_NOT_REQUESTED", lang),
  272. new { }
  273. );
  274. }
  275. // RESEND has reduced cooldown: 60 seconds (vs 60 seconds for RequestOtp)
  276. int minSecondsBetweenResend = 60;
  277. var secondsSinceLastRequest = (DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)).TotalSeconds;
  278. if (secondsSinceLastRequest < minSecondsBetweenResend)
  279. {
  280. var waitSeconds = (int)(minSecondsBetweenResend - secondsSinceLastRequest);
  281. log.Warn($"ResendOtp: Too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago.");
  282. return DotnetLib.Http.HttpResponse.BuildResponse(
  283. log,
  284. url,
  285. json,
  286. CommonErrorCode.OtpTooManyRequests,
  287. $"Please wait {waitSeconds} seconds before resending OTP.",
  288. new {
  289. waitSeconds = waitSeconds,
  290. canResendAt = existingOtp.CreatedDate?.AddSeconds(minSecondsBetweenResend)
  291. }
  292. );
  293. }
  294. // Generate new 6-digit OTP (fixed 111111 for test account abc@gmail.com)
  295. bool isTestAccount = request.email.ToLower() == "abc@gmail.com";
  296. string otpCode = isTestAccount ? "111111" : GenerateOtp();
  297. // OTP expires in 5 minutes
  298. int otpExpireMinutes = 5;
  299. // Get customer ID (should exist since OTP record exists)
  300. var customer = dbContext.CustomerInfos
  301. .Where(c => c.Email == request.email)
  302. .FirstOrDefault();
  303. decimal? customerId = customer?.Id ?? existingOtp.CustomerId;
  304. // UPDATE existing OTP record with new code and expiry
  305. existingOtp.OtpCode = otpCode;
  306. existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
  307. existingOtp.AttemptCount = 0; // Reset attempt count
  308. existingOtp.IsUsed = false; // Reset to unused
  309. existingOtp.CustomerId = customerId;
  310. existingOtp.CreatedDate = DateTime.Now; // Update to track resend time
  311. log.Info($"ResendOtp: Updated OTP record ID={existingOtp.Id} for {request.email}");
  312. await dbContext.SaveChangesAsync();
  313. // Skip email sending for test account
  314. if (!isTestAccount)
  315. {
  316. // Add to MESSAGE_QUEUE for background email sending
  317. string templateCode = "OTP_LOGIN";
  318. var template = dbContext.MessageTemplates
  319. .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value);
  320. if (template == null)
  321. {
  322. log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE");
  323. throw new Exception($"Email template '{templateCode}' not found");
  324. }
  325. // Get subject based on language
  326. string emailSubject = lang == "en" ? (template.SubjectEn ?? template.Subject ?? "")
  327. : lang == "vi" ? (template.Subject ?? "")
  328. : (template.SubjectLo ?? template.Subject ?? "");
  329. // Get content based on language
  330. string emailContent = lang == "en" ? (template.ContentEn ?? template.Content ?? "")
  331. : lang == "vi" ? (template.Content ?? "")
  332. : (template.ContentLo ?? template.Content ?? "");
  333. // Replace placeholders
  334. emailContent = emailContent
  335. .Replace("{{OTP_CODE}}", otpCode)
  336. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  337. emailSubject = emailSubject
  338. .Replace("{{OTP_CODE}}", otpCode)
  339. .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
  340. var emailMessageID = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
  341. var emailMessage = new MessageQueue
  342. {
  343. Id = emailMessageID,
  344. MessageType = 1, // Email
  345. Recipient = request.email,
  346. Subject = emailSubject,
  347. Content = emailContent,
  348. Priority = true, // High priority
  349. Status = 0, // Pending
  350. ScheduledAt = DateTime.Now,
  351. RetryCount = 0,
  352. MaxRetry = 3,
  353. CreatedBy = customerId,
  354. CreatedDate = DateTime.Now
  355. };
  356. dbContext.MessageQueues.Add(emailMessage);
  357. await dbContext.SaveChangesAsync();
  358. }
  359. log.Info($"ResendOtp: OTP resent for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
  360. return DotnetLib.Http.HttpResponse.BuildResponse(
  361. log,
  362. url,
  363. json,
  364. CommonErrorCode.Success,
  365. ConfigManager.Instance.GetConfigWebValue("OTP_RESENT_SUCCESS", lang),
  366. new
  367. {
  368. email = request.email,
  369. expireInSeconds = otpExpireMinutes * 60
  370. }
  371. );
  372. }
  373. catch (Exception exception)
  374. {
  375. log.Error("ResendOtp Exception: ", exception);
  376. }
  377. return DotnetLib.Http.HttpResponse.BuildResponse(
  378. log,
  379. url,
  380. json,
  381. CommonErrorCode.SystemError,
  382. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  383. new { }
  384. );
  385. }
  386. /// <summary>
  387. /// Verify OTP and complete login - return JWT token
  388. /// </summary>
  389. public async Task<IActionResult> VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request)
  390. {
  391. var url = httpRequest.Path;
  392. var json = JsonConvert.SerializeObject(request);
  393. log.Debug("URL: " + url + " => Request: " + json);
  394. try
  395. {
  396. if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode))
  397. {
  398. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  399. return DotnetLib.Http.HttpResponse.BuildResponse(
  400. log,
  401. url,
  402. json,
  403. CommonErrorCode.RequiredFieldMissing,
  404. ConfigManager.Instance.GetConfigWebValue("EMAIL_OTP_REQUIRED", lang),
  405. new { }
  406. );
  407. }
  408. // Get language for response messages
  409. string responseLang = CommonLogic.GetLanguage(httpRequest, request.lang);
  410. // Find valid OTP
  411. var otpRecord = dbContext.OtpVerifications
  412. .Where(o => o.UserEmail == request.email
  413. && o.OtpCode == request.otpCode
  414. && o.IsUsed == false
  415. && o.ExpiredAt > DateTime.Now)
  416. .OrderByDescending(o => o.CreatedDate)
  417. .FirstOrDefault();
  418. if (otpRecord == null)
  419. {
  420. // Check if OTP exists but expired or used
  421. var anyOtp = dbContext.OtpVerifications
  422. .Where(o => o.UserEmail == request.email && o.OtpCode == request.otpCode)
  423. .FirstOrDefault();
  424. if (anyOtp != null)
  425. {
  426. if (anyOtp.IsUsed == true)
  427. {
  428. return DotnetLib.Http.HttpResponse.BuildResponse(
  429. log,
  430. url,
  431. json,
  432. CommonErrorCode.OtpAlreadyUsed,
  433. ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang),
  434. new { }
  435. );
  436. }
  437. if (anyOtp.ExpiredAt <= DateTime.Now)
  438. {
  439. return DotnetLib.Http.HttpResponse.BuildResponse(
  440. log,
  441. url,
  442. json,
  443. CommonErrorCode.OtpExpired,
  444. ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang),
  445. new { }
  446. );
  447. }
  448. }
  449. return DotnetLib.Http.HttpResponse.BuildResponse(
  450. log,
  451. url,
  452. json,
  453. CommonErrorCode.OtpInvalid,
  454. ConfigManager.Instance.GetConfigWebValue("OTP_INVALID", responseLang),
  455. new { }
  456. );
  457. }
  458. // Mark OTP as used
  459. otpRecord.IsUsed = true;
  460. // Get customer info
  461. var customer = dbContext.CustomerInfos
  462. .Where(c => c.Email == request.email)
  463. .FirstOrDefault();
  464. if (customer == null)
  465. {
  466. return DotnetLib.Http.HttpResponse.BuildResponse(
  467. log,
  468. url,
  469. json,
  470. CommonErrorCode.UserNotFound,
  471. ConfigManager.Instance.GetConfigWebValue("USER_NOT_FOUND", responseLang),
  472. new { }
  473. );
  474. }
  475. // Update customer verification status
  476. customer.IsVerified = true;
  477. customer.LastLoginDate = DateTime.Now;
  478. customer.LastUpdate = DateTime.Now;
  479. // Generate JWT tokens
  480. int tokenExpireHours = 24;
  481. int refreshTokenExpireDays = 30;
  482. string accessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
  483. string refreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
  484. var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
  485. var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
  486. // Revoke old tokens
  487. var oldTokens = dbContext.UserTokens
  488. .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
  489. .ToList();
  490. foreach (var oldToken in oldTokens)
  491. {
  492. oldToken.IsRevoked = true;
  493. }
  494. // Save new token
  495. var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
  496. var userToken = new UserToken
  497. {
  498. Id = tokenId,
  499. CustomerId = customer.Id,
  500. AccessToken = accessToken,
  501. RefreshToken = refreshToken,
  502. TokenType = "Bearer",
  503. DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
  504. IpAddress = GetClientIpAddress(httpRequest),
  505. ExpiredAt = expiresAt,
  506. RefreshExpiredAt = refreshExpiresAt,
  507. IsRevoked = false,
  508. CreatedDate = DateTime.Now,
  509. LastUsed = DateTime.Now
  510. };
  511. dbContext.UserTokens.Add(userToken);
  512. await dbContext.SaveChangesAsync();
  513. return DotnetLib.Http.HttpResponse.BuildResponse(
  514. log,
  515. url,
  516. json,
  517. CommonErrorCode.Success,
  518. ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", responseLang),
  519. new
  520. {
  521. userId = customer.Id,
  522. email = customer.Email ?? "",
  523. fullName = $"{customer.SurName} {customer.LastName}".Trim(),
  524. avatarUrl = customer.AvatarUrl,
  525. accessToken,
  526. refreshToken,
  527. expiresAt
  528. }
  529. );
  530. }
  531. catch (Exception exception)
  532. {
  533. log.Error("Exception: ", exception);
  534. }
  535. return DotnetLib.Http.HttpResponse.BuildResponse(
  536. log,
  537. url,
  538. json,
  539. CommonErrorCode.SystemError,
  540. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  541. new { }
  542. );
  543. }
  544. /// <summary>
  545. /// Generate 6-digit OTP code
  546. /// </summary>
  547. private string GenerateOtp()
  548. {
  549. using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
  550. {
  551. var bytes = new byte[4];
  552. rng.GetBytes(bytes);
  553. var number = Math.Abs(BitConverter.ToInt32(bytes, 0)) % 1000000;
  554. return number.ToString("D6");
  555. }
  556. }
  557. /// <summary>
  558. /// Get client IP address
  559. /// </summary>
  560. private string GetClientIpAddress(HttpRequest httpRequest)
  561. {
  562. var ipAddress = httpRequest.Headers["X-Forwarded-For"].FirstOrDefault();
  563. if (string.IsNullOrEmpty(ipAddress))
  564. {
  565. ipAddress = httpRequest.HttpContext.Connection.RemoteIpAddress?.ToString();
  566. }
  567. return ipAddress ?? "Unknown";
  568. }
  569. public async Task<IActionResult> GoogleLogin(HttpRequest httpRequest, GoogleLoginReq request)
  570. {
  571. var url = httpRequest.Path;
  572. var json = JsonConvert.SerializeObject(request);
  573. log.Debug("URL: " + url + " => GoogleLogin Request: " + json);
  574. try
  575. {
  576. string lang = CommonLogic.GetLanguage(httpRequest, request?.lang);
  577. var clientId = configuration["Google:ClientId"];
  578. var redirectUri = configuration["Google:RedirectUri"];
  579. if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(redirectUri))
  580. {
  581. log.Error("Google Auth configuration missing");
  582. return DotnetLib.Http.HttpResponse.BuildResponse(
  583. log,
  584. url,
  585. json,
  586. CommonErrorCode.SystemError,
  587. ConfigManager.Instance.GetConfigWebValue("GOOGLE_CONFIG_MISSING", lang),
  588. new { }
  589. );
  590. }
  591. var googleUrl = $"https://accounts.google.com/o/oauth2/v2/auth?client_id={clientId}&redirect_uri={redirectUri}&response_type=code&scope=email%20profile&prompt=select_account";
  592. return DotnetLib.Http.HttpResponse.BuildResponse(
  593. log,
  594. url,
  595. json,
  596. CommonErrorCode.Success,
  597. ConfigManager.Instance.GetConfigWebValue("SUCCESS", lang),
  598. new { url = googleUrl }
  599. );
  600. }
  601. catch (Exception ex)
  602. {
  603. log.Error("GoogleLogin Exception: ", ex);
  604. return DotnetLib.Http.HttpResponse.BuildResponse(
  605. log,
  606. url,
  607. json,
  608. CommonErrorCode.SystemError,
  609. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  610. new { }
  611. );
  612. }
  613. }
  614. public async Task<IActionResult> GoogleCallback(HttpRequest httpRequest, GoogleCallbackReq request)
  615. {
  616. var url = httpRequest.Path;
  617. var json = JsonConvert.SerializeObject(request);
  618. log.Debug("URL: " + url + " => GoogleCallback Request: " + json);
  619. try
  620. {
  621. // Get language for response messages
  622. string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
  623. if (string.IsNullOrEmpty(request.code))
  624. {
  625. return DotnetLib.Http.HttpResponse.BuildResponse(
  626. log,
  627. url,
  628. json,
  629. CommonErrorCode.RequiredFieldMissing,
  630. ConfigManager.Instance.GetConfigWebValue("GOOGLE_CODE_REQUIRED", lang),
  631. new { }
  632. );
  633. }
  634. var clientId = configuration["Google:ClientId"];
  635. var clientSecret = configuration["Google:ClientSecret"];
  636. var redirectUri = !string.IsNullOrEmpty(request.redirectUri) ? request.redirectUri : configuration["Google:RedirectUri"];
  637. using (var httpClient = new HttpClient())
  638. {
  639. // 1. Exchange code for token
  640. var tokenRequestContent = new FormUrlEncodedContent(new[]
  641. {
  642. new KeyValuePair<string, string>("code", request.code),
  643. new KeyValuePair<string, string>("client_id", clientId),
  644. new KeyValuePair<string, string>("client_secret", clientSecret),
  645. new KeyValuePair<string, string>("redirect_uri", redirectUri),
  646. new KeyValuePair<string, string>("grant_type", "authorization_code")
  647. });
  648. var tokenResponse = await httpClient.PostAsync("https://oauth2.googleapis.com/token", tokenRequestContent);
  649. var tokenResponseString = await tokenResponse.Content.ReadAsStringAsync();
  650. if (!tokenResponse.IsSuccessStatusCode)
  651. {
  652. log.Error($"Google Token Exchange Failed: {tokenResponseString}");
  653. return DotnetLib.Http.HttpResponse.BuildResponse(
  654. log,
  655. url,
  656. json,
  657. CommonErrorCode.ExternalServiceError,
  658. ConfigManager.Instance.GetConfigWebValue("GOOGLE_TOKEN_EXCHANGE_FAILED", lang),
  659. new { error = tokenResponseString }
  660. );
  661. }
  662. var tokenData = JsonConvert.DeserializeObject<JObject>(tokenResponseString);
  663. var accessToken = tokenData["access_token"]?.ToString();
  664. if (string.IsNullOrEmpty(accessToken))
  665. {
  666. return DotnetLib.Http.HttpResponse.BuildResponse(
  667. log,
  668. url,
  669. json,
  670. CommonErrorCode.ExternalServiceError,
  671. ConfigManager.Instance.GetConfigWebValue("GOOGLE_NO_ACCESS_TOKEN", lang),
  672. new { }
  673. );
  674. }
  675. // 2. Get User Info
  676. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
  677. var userInfoResponse = await httpClient.GetAsync("https://www.googleapis.com/oauth2/v2/userinfo");
  678. var userInfoString = await userInfoResponse.Content.ReadAsStringAsync();
  679. if (!userInfoResponse.IsSuccessStatusCode)
  680. {
  681. log.Error($"Google User Info Failed: {userInfoString}");
  682. return DotnetLib.Http.HttpResponse.BuildResponse(
  683. log,
  684. url,
  685. json,
  686. CommonErrorCode.ExternalServiceError,
  687. ConfigManager.Instance.GetConfigWebValue("GOOGLE_USERINFO_FAILED", lang),
  688. new { error = userInfoString }
  689. );
  690. }
  691. var userInfo = JsonConvert.DeserializeObject<JObject>(userInfoString);
  692. var email = userInfo["email"]?.ToString();
  693. var name = userInfo["name"]?.ToString(); // Full name
  694. var picture = userInfo["picture"]?.ToString();
  695. if (string.IsNullOrEmpty(email))
  696. {
  697. return DotnetLib.Http.HttpResponse.BuildResponse(
  698. log,
  699. url,
  700. json,
  701. CommonErrorCode.ExternalServiceError,
  702. ConfigManager.Instance.GetConfigWebValue("GOOGLE_NO_EMAIL", lang),
  703. new { }
  704. );
  705. }
  706. // 3. Login or Register logic
  707. var customer = dbContext.CustomerInfos
  708. .FirstOrDefault(c => c.Email == email);
  709. if (customer == null)
  710. {
  711. // Register new user
  712. var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
  713. // Try to split name
  714. string surname = name;
  715. string lastname = "";
  716. if (!string.IsNullOrEmpty(name))
  717. {
  718. var parts = name.Split(' ');
  719. if (parts.Length > 1)
  720. {
  721. lastname = parts[parts.Length - 1];
  722. surname = string.Join(" ", parts.Take(parts.Length - 1));
  723. }
  724. }
  725. else
  726. {
  727. surname = email.Split('@')[0];
  728. }
  729. customer = new CustomerInfo
  730. {
  731. Id = newCustomerId,
  732. Email = email,
  733. SurName = surname,
  734. LastName = lastname,
  735. AvatarUrl = picture,
  736. Status = true,
  737. IsVerified = true, // Verified by Google
  738. CreatedDate = DateTime.Now,
  739. LastUpdate = DateTime.Now
  740. };
  741. dbContext.CustomerInfos.Add(customer);
  742. await dbContext.SaveChangesAsync();
  743. log.Info($"Created new customer via Google Login: ID={newCustomerId}, Email={email}");
  744. }
  745. else
  746. {
  747. // Update existing user info if needed or just log them in
  748. customer.IsVerified = true;
  749. if (string.IsNullOrEmpty(customer.AvatarUrl) && !string.IsNullOrEmpty(picture))
  750. {
  751. customer.AvatarUrl = picture;
  752. }
  753. customer.LastLoginDate = DateTime.Now;
  754. customer.LastUpdate = DateTime.Now;
  755. await dbContext.SaveChangesAsync();
  756. log.Info($"Existing customer logged in via Google: ID={customer.Id}, Email={email}");
  757. }
  758. // 4. Generate JWT
  759. int tokenExpireHours = 24;
  760. int refreshTokenExpireDays = 30;
  761. string jwtAccessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
  762. string jwtRefreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
  763. var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
  764. var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
  765. // Revoke old tokens
  766. var oldTokens = dbContext.UserTokens
  767. .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
  768. .ToList();
  769. foreach (var oldToken in oldTokens)
  770. {
  771. oldToken.IsRevoked = true;
  772. }
  773. // Save new token
  774. var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
  775. var userToken = new UserToken
  776. {
  777. Id = tokenId,
  778. CustomerId = customer.Id,
  779. AccessToken = jwtAccessToken,
  780. RefreshToken = jwtRefreshToken,
  781. TokenType = "Bearer",
  782. DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
  783. IpAddress = GetClientIpAddress(httpRequest),
  784. ExpiredAt = expiresAt,
  785. RefreshExpiredAt = refreshExpiresAt,
  786. IsRevoked = false,
  787. CreatedDate = DateTime.Now,
  788. LastUsed = DateTime.Now
  789. };
  790. dbContext.UserTokens.Add(userToken);
  791. await dbContext.SaveChangesAsync();
  792. return DotnetLib.Http.HttpResponse.BuildResponse(
  793. log,
  794. url,
  795. json,
  796. CommonErrorCode.Success,
  797. ConfigManager.Instance.GetConfigWebValue("GOOGLE_LOGIN_SUCCESS", lang),
  798. new
  799. {
  800. userId = customer.Id,
  801. email = customer.Email ?? "",
  802. fullName = $"{customer.SurName} {customer.LastName}".Trim(),
  803. avatarUrl = customer.AvatarUrl,
  804. accessToken = jwtAccessToken,
  805. refreshToken = jwtRefreshToken,
  806. expiresAt
  807. }
  808. );
  809. }
  810. }
  811. catch (Exception exception)
  812. {
  813. log.Error("GoogleCallback Exception: ", exception);
  814. return DotnetLib.Http.HttpResponse.BuildResponse(
  815. log,
  816. url,
  817. json,
  818. CommonErrorCode.SystemError,
  819. ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
  820. new { }
  821. );
  822. }
  823. }
  824. }
  825. }