UserBusinessImpl.cs 43 KB

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