UserBusinessImpl.cs 49 KB

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