Bläddra i källkod

Add Google OAuth login and callback endpoints

Implemented Google OAuth login and callback logic in UserBusinessImpl and exposed new endpoints in UserController. Refactored and improved OTP logic, updated project references, and made minor code style and formatting improvements. Updated documentation diagrams and fixed some configuration and path issues.
hieubt 2 veckor sedan
förälder
incheckning
1f6c0f1082

+ 6 - 23
EsimLao/Common/Constant/CommonConstant.cs

@@ -17,7 +17,6 @@ public static class CommonConstant
     public const string NextQuestionFail = "-8";
     public const string OtpWrong = "-9";
 
-
     public const string Unauthorized = "-401";
     public const string BadRequest = "-402";
 
@@ -29,7 +28,6 @@ public static class CommonConstant
     public const string ANSWER_RIGHT = "RIGHT";
     public const string ANSWER_WRONG = "WRONG";
 
-
     public const String PathImage = "/outside";
 
     public const String MissitonTypeLOGIN = "LOGIN";
@@ -37,7 +35,6 @@ public static class CommonConstant
     public const String MissitonTypeREGISTERS = "REGISTERS";
     public const String MissitonTypeINVITING = "INVITING";
 
-
     public const int StatusActive = 1;
     public const int StatusInactive = 0;
     public const int StatusNotClaimed = 2;
@@ -58,13 +55,11 @@ public static class CommonConstant
     public const string TypeInviting = "INVITING";
     public const string TypeRenewService = "RENEW";
 
-
     public const string PathOutside = "PathOutside";
 
     public const string CheckSub = "CHECKSUB";
     public const string Subscribe = "SUBSCRIBE";
 
-
     public static readonly List<int> ListStatusInt = new List<int> { StatusActive, StatusInactive };
     public static readonly List<string> ListCampaignType = new List<string> { "EU" };
     public static readonly List<string> ListMissionType = new List<string>
@@ -83,9 +78,10 @@ public static class CommonConstant
         "REST_POST",
         "REST_GET"
     };
-    public static readonly List<string> ListServiceActionCode = new List<string> { 
+    public static readonly List<string> ListServiceActionCode = new List<string>
+    {
         "REGISTER",
-        "BUY_TURN", 
+        "BUY_TURN",
     };
 
     public static readonly List<string> ListVendorPackagePeriod = new List<string>
@@ -102,18 +98,9 @@ public static class CommonConstant
         "VIP"
     };
 
-    public static readonly List<string> ListMoneyType = new List<string>
-    {
-        "CENT",
-        "USD",
-        "EUR"
-    };
+    public static readonly List<string> ListMoneyType = new List<string> { "CENT", "USD", "EUR" };
 
-    public static readonly List<string> ListPrizeType = new List<string>
-    {
-        "POINT",
-        "MONEY"
-    };
+    public static readonly List<string> ListPrizeType = new List<string> { "POINT", "MONEY" };
 
     public const string DateTimeFormar1 = "dd/MM/yyyy HH:mm:ss";
     public const string DateTimeFormar2 = "MMMM yyyy";
@@ -298,9 +285,5 @@ public static class CommonErrorCode
     public const string DuplicateEntry = "-804";
 
     // Prize Types (kept for backward compatibility)
-    public static readonly List<string> ListPrizeType = new List<string>
-    {
-        "POINT",
-        "MONEY"
-    };
+    public static readonly List<string> ListPrizeType = new List<string> { "POINT", "MONEY" };
 }

+ 251 - 156
EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs

@@ -1,22 +1,22 @@
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
 using Common;
 using Common.Constant;
 using Common.Http;
 using Common.Logic;
-//using Esim.Apis.Logic;
-using Esim.Apis.Singleton;
 using Database;
 using Database.Database;
+//using Esim.Apis.Logic;
+using Esim.Apis.Singleton;
 using log4net;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.VisualBasic;
 using Newtonsoft.Json;
-using System;
-using System.Threading.Tasks;
-using System.Xml.Serialization;
-using System.Net.Http;
-using System.Net.Http.Headers;
 using Newtonsoft.Json.Linq;
-using System.Linq;
 
 namespace Esim.Apis.Business
 {
@@ -40,7 +40,6 @@ namespace Esim.Apis.Business
             return configuration.GetSection(key).Value ?? "";
         }
 
-
         /// <summary>
         /// Request OTP to be sent to email
         /// </summary>
@@ -68,8 +67,8 @@ namespace Esim.Apis.Business
                 string otpCode = isTestAccount ? "111111" : GenerateOtp();
 
                 // Check if customer exists, if not create new
-                var customer = dbContext.CustomerInfos
-                    .Where(c => c.Email == request.email)
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == request.email)
                     .FirstOrDefault();
 
                 decimal? customerId = customer?.Id;
@@ -77,11 +76,14 @@ namespace Esim.Apis.Business
                 if (customer == null)
                 {
                     // Create new customer record - manually get ID from Oracle sequence
-                    var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
-                    
+                    var newCustomerId = await Database.DbLogic.GenIdAsync(
+                        dbContext,
+                        "CUSTOMER_INFO_SEQ"
+                    );
+
                     // Extract name from email (part before @)
                     string emailUsername = request.email.Split('@')[0];
-                    
+
                     var newCustomer = new CustomerInfo
                     {
                         Id = newCustomerId,
@@ -101,48 +103,60 @@ namespace Esim.Apis.Business
                 // Check if there's an existing OTP record for this email (ANY record, used or unused)
                 int otpExpireMinutes = 5;
                 int minSecondsBetweenRequests = 60; // Anti-spam: minimum 60 seconds between requests
-                
-                var existingOtp = dbContext.OtpVerifications
-                    .Where(o => o.UserEmail == request.email)
+
+                var existingOtp = dbContext
+                    .OtpVerifications.Where(o => o.UserEmail == request.email)
                     .OrderByDescending(o => o.CreatedDate)
                     .FirstOrDefault();
 
                 if (existingOtp != null)
                 {
                     // Anti-spam check: prevent rapid OTP requests
-                    var secondsSinceLastRequest = (DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)).TotalSeconds;
+                    var secondsSinceLastRequest = (
+                        DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)
+                    ).TotalSeconds;
                     if (secondsSinceLastRequest < minSecondsBetweenRequests)
                     {
-                        var waitSeconds = (int)(minSecondsBetweenRequests - secondsSinceLastRequest);
-                        log.Warn($"Spam prevention: OTP request too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago.");
-                        
+                        var waitSeconds = (int)(
+                            minSecondsBetweenRequests - secondsSinceLastRequest
+                        );
+                        log.Warn(
+                            $"Spam prevention: OTP request too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago."
+                        );
+
                         return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.OtpTooManyRequests,
                             $"Please wait {waitSeconds} seconds before requesting a new OTP.",
-                            new { 
+                            new
+                            {
                                 waitSeconds = waitSeconds,
-                                canRequestAt = existingOtp.CreatedDate?.AddSeconds(minSecondsBetweenRequests)
+                                canRequestAt = existingOtp.CreatedDate?.AddSeconds(
+                                    minSecondsBetweenRequests
+                                )
                             }
                         );
                     }
-                    
+
                     // UPDATE existing record - reuse for this email to prevent table bloat
                     existingOtp.OtpCode = otpCode;
                     existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
                     existingOtp.AttemptCount = 0; // Reset attempt count
-                    existingOtp.IsUsed = false;   // Reset to unused (allow reuse)
+                    existingOtp.IsUsed = false; // Reset to unused (allow reuse)
                     existingOtp.CustomerId = customerId; // Update customer ID if changed
                     existingOtp.CreatedDate = DateTime.Now; // Update to current time
-                    
-                    log.Info($"Updated existing OTP record ID={existingOtp.Id} for {request.email} (was {(existingOtp.IsUsed == true ? "used" : "unused")})");
+
+                    log.Info(
+                        $"Updated existing OTP record ID={existingOtp.Id} for {request.email} (was {(existingOtp.IsUsed == true ? "used" : "unused")})"
+                    );
                 }
                 else
                 {
                     // INSERT new record only for first-time email
-                    var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
+                    var otpId = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
                     var otpVerification = new OtpVerification
                     {
                         Id = otpId,
@@ -171,8 +185,9 @@ namespace Esim.Apis.Business
                     string templateCode = "OTP_LOGIN";
 
                     // Query template and get language-specific content
-                    var template = dbContext.MessageTemplates
-                        .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value);
+                    var template = dbContext.MessageTemplates.FirstOrDefault(
+                        t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value
+                    );
 
                     if (template == null)
                     {
@@ -181,14 +196,20 @@ namespace Esim.Apis.Business
                     }
 
                     // Get subject based on language: vi=default, en=_EN, lo=_LO (default)
-                    string emailSubject = lang == "en" ? (template.SubjectEn ?? template.Subject ?? "")
-                                        : lang == "vi" ? (template.Subject ?? "")
-                                        : (template.SubjectLo ?? template.Subject ?? "");
+                    string emailSubject =
+                        lang == "en"
+                            ? (template.SubjectEn ?? template.Subject ?? "")
+                            : lang == "vi"
+                                ? (template.Subject ?? "")
+                                : (template.SubjectLo ?? template.Subject ?? "");
 
                     // Get content based on language: vi=default, en=_EN, lo=_LO (default)
-                    string emailContent = lang == "en" ? (template.ContentEn ?? template.Content ?? "")
-                                        : lang == "vi" ? (template.Content ?? "")
-                                        : (template.ContentLo ?? template.Content ?? "");
+                    string emailContent =
+                        lang == "en"
+                            ? (template.ContentEn ?? template.Content ?? "")
+                            : lang == "vi"
+                                ? (template.Content ?? "")
+                                : (template.ContentLo ?? template.Content ?? "");
 
                     // Replace placeholders in content
                     emailContent = emailContent
@@ -199,14 +220,15 @@ namespace Esim.Apis.Business
                     emailSubject = emailSubject
                         .Replace("{{OTP_CODE}}", otpCode)
                         .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
-                    var emailMessageID = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
+                    var emailMessageID = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
                     var emailMessage = new MessageQueue
                     {
                         Id = emailMessageID,
                         MessageType = 1, // Email
                         Recipient = request.email,
-                        Subject = emailSubject,     // Pre-resolved subject
-                        Content = emailContent,     // Pre-resolved content
+                        Subject = emailSubject, // Pre-resolved subject
+                        Content = emailContent, // Pre-resolved content
                         Priority = true, // High priority
                         Status = 0, // Pending
                         ScheduledAt = DateTime.Now,
@@ -220,7 +242,9 @@ namespace Esim.Apis.Business
                     await dbContext.SaveChangesAsync();
                 }
 
-                log.Info($"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
+                log.Info(
+                    $"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}"
+                );
 
                 //return DotnetLib.Http.HttpResponse.BuildResponse(
                 //    log,
@@ -241,11 +265,7 @@ namespace Esim.Apis.Business
                     json,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
-                    new
-                    {
-                        email = request.email,
-                        expireInSeconds = otpExpireMinutes * 60
-                    }
+                    new { email = request.email, expireInSeconds = otpExpireMinutes * 60 }
                 );
             }
             catch (Exception exception)
@@ -271,11 +291,11 @@ namespace Esim.Apis.Business
             var url = httpRequest.Path;
             var json = JsonConvert.SerializeObject(request);
             log.Debug("URL: " + url + " => ResendOtp Request: " + json);
-            
+
             try
             {
                 string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
-                
+
                 // Validate email is required
                 if (string.IsNullOrEmpty(request.email))
                 {
@@ -290,8 +310,8 @@ namespace Esim.Apis.Business
                 }
 
                 // Check if there's an existing OTP record for this email
-                var existingOtp = dbContext.OtpVerifications
-                    .Where(o => o.UserEmail == request.email)
+                var existingOtp = dbContext
+                    .OtpVerifications.Where(o => o.UserEmail == request.email)
                     .OrderByDescending(o => o.CreatedDate)
                     .FirstOrDefault();
 
@@ -311,22 +331,29 @@ namespace Esim.Apis.Business
 
                 // RESEND has reduced cooldown: 60 seconds (vs 60 seconds for RequestOtp)
                 int minSecondsBetweenResend = 60;
-                var secondsSinceLastRequest = (DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)).TotalSeconds;
-                
+                var secondsSinceLastRequest = (
+                    DateTime.Now - (existingOtp.CreatedDate ?? DateTime.Now)
+                ).TotalSeconds;
+
                 if (secondsSinceLastRequest < minSecondsBetweenResend)
                 {
                     var waitSeconds = (int)(minSecondsBetweenResend - secondsSinceLastRequest);
-                    log.Warn($"ResendOtp: Too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago.");
-                    
+                    log.Warn(
+                        $"ResendOtp: Too soon for {request.email}. Last request {secondsSinceLastRequest:F0}s ago."
+                    );
+
                     return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         url,
                         json,
                         CommonErrorCode.OtpTooManyRequests,
                         $"Please wait {waitSeconds} seconds before resending OTP.",
-                        new { 
+                        new
+                        {
                             waitSeconds = waitSeconds,
-                            canResendAt = existingOtp.CreatedDate?.AddSeconds(minSecondsBetweenResend)
+                            canResendAt = existingOtp.CreatedDate?.AddSeconds(
+                                minSecondsBetweenResend
+                            )
                         }
                     );
                 }
@@ -334,13 +361,13 @@ namespace Esim.Apis.Business
                 // Generate new 6-digit OTP (fixed 111111 for test account abc@gmail.com)
                 bool isTestAccount = request.email.ToLower() == "abc@gmail.com";
                 string otpCode = isTestAccount ? "111111" : GenerateOtp();
-                
+
                 // OTP expires in 5 minutes
                 int otpExpireMinutes = 5;
 
                 // Get customer ID (should exist since OTP record exists)
-                var customer = dbContext.CustomerInfos
-                    .Where(c => c.Email == request.email)
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == request.email)
                     .FirstOrDefault();
                 decimal? customerId = customer?.Id ?? existingOtp.CustomerId;
 
@@ -348,12 +375,12 @@ namespace Esim.Apis.Business
                 existingOtp.OtpCode = otpCode;
                 existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
                 existingOtp.AttemptCount = 0; // Reset attempt count
-                existingOtp.IsUsed = false;   // Reset to unused
+                existingOtp.IsUsed = false; // Reset to unused
                 existingOtp.CustomerId = customerId;
                 existingOtp.CreatedDate = DateTime.Now; // Update to track resend time
-                
+
                 log.Info($"ResendOtp: Updated OTP record ID={existingOtp.Id} for {request.email}");
-                
+
                 await dbContext.SaveChangesAsync();
 
                 // Skip email sending for test account
@@ -361,9 +388,10 @@ namespace Esim.Apis.Business
                 {
                     // Add to MESSAGE_QUEUE for background email sending
                     string templateCode = "OTP_LOGIN";
-                    
-                    var template = dbContext.MessageTemplates
-                        .FirstOrDefault(t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value);
+
+                    var template = dbContext.MessageTemplates.FirstOrDefault(
+                        t => t.TemplateCode == templateCode && t.Status.HasValue && t.Status.Value
+                    );
 
                     if (template == null)
                     {
@@ -372,14 +400,20 @@ namespace Esim.Apis.Business
                     }
 
                     // Get subject based on language
-                    string emailSubject = lang == "en" ? (template.SubjectEn ?? template.Subject ?? "")
-                                        : lang == "vi" ? (template.Subject ?? "")
-                                        : (template.SubjectLo ?? template.Subject ?? "");
+                    string emailSubject =
+                        lang == "en"
+                            ? (template.SubjectEn ?? template.Subject ?? "")
+                            : lang == "vi"
+                                ? (template.Subject ?? "")
+                                : (template.SubjectLo ?? template.Subject ?? "");
 
                     // Get content based on language
-                    string emailContent = lang == "en" ? (template.ContentEn ?? template.Content ?? "")
-                                        : lang == "vi" ? (template.Content ?? "")
-                                        : (template.ContentLo ?? template.Content ?? "");
+                    string emailContent =
+                        lang == "en"
+                            ? (template.ContentEn ?? template.Content ?? "")
+                            : lang == "vi"
+                                ? (template.Content ?? "")
+                                : (template.ContentLo ?? template.Content ?? "");
 
                     // Replace placeholders
                     emailContent = emailContent
@@ -390,7 +424,8 @@ namespace Esim.Apis.Business
                         .Replace("{{OTP_CODE}}", otpCode)
                         .Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
 
-                    var emailMessageID = (int)await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
+                    var emailMessageID = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "MESSAGE_QUEUE_SEQ");
                     var emailMessage = new MessageQueue
                     {
                         Id = emailMessageID,
@@ -411,7 +446,9 @@ namespace Esim.Apis.Business
                     await dbContext.SaveChangesAsync();
                 }
 
-                log.Info($"ResendOtp: OTP resent for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
+                log.Info(
+                    $"ResendOtp: OTP resent for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}"
+                );
 
                 return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
@@ -419,11 +456,7 @@ namespace Esim.Apis.Business
                     json,
                     CommonErrorCode.Success,
                     ConfigManager.Instance.GetConfigWebValue("OTP_RESENT_SUCCESS", lang),
-                    new
-                    {
-                        email = request.email,
-                        expireInSeconds = otpExpireMinutes * 60
-                    }
+                    new { email = request.email, expireInSeconds = otpExpireMinutes * 60 }
                 );
             }
             catch (Exception exception)
@@ -467,19 +500,24 @@ namespace Esim.Apis.Business
                 string responseLang = CommonLogic.GetLanguage(httpRequest, request.lang);
 
                 // Find valid OTP
-                var otpRecord = dbContext.OtpVerifications
-                    .Where(o => o.UserEmail == request.email
-                        && o.OtpCode == request.otpCode
-                        && o.IsUsed == false
-                        && o.ExpiredAt > DateTime.Now)
+                var otpRecord = dbContext
+                    .OtpVerifications.Where(
+                        o =>
+                            o.UserEmail == request.email
+                            && o.OtpCode == request.otpCode
+                            && o.IsUsed == false
+                            && o.ExpiredAt > DateTime.Now
+                    )
                     .OrderByDescending(o => o.CreatedDate)
                     .FirstOrDefault();
 
                 if (otpRecord == null)
                 {
                     // Check if OTP exists but expired or used
-                    var anyOtp = dbContext.OtpVerifications
-                        .Where(o => o.UserEmail == request.email && o.OtpCode == request.otpCode)
+                    var anyOtp = dbContext
+                        .OtpVerifications.Where(
+                            o => o.UserEmail == request.email && o.OtpCode == request.otpCode
+                        )
                         .FirstOrDefault();
 
                     if (anyOtp != null)
@@ -491,7 +529,10 @@ namespace Esim.Apis.Business
                                 url,
                                 json,
                                 CommonErrorCode.OtpAlreadyUsed,
-                                ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang),
+                                ConfigManager.Instance.GetConfigWebValue(
+                                    "OTP_ALREADY_USED",
+                                    responseLang
+                                ),
                                 new { }
                             );
                         }
@@ -502,7 +543,10 @@ namespace Esim.Apis.Business
                                 url,
                                 json,
                                 CommonErrorCode.OtpExpired,
-                                ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang),
+                                ConfigManager.Instance.GetConfigWebValue(
+                                    "OTP_EXPIRED",
+                                    responseLang
+                                ),
                                 new { }
                             );
                         }
@@ -522,8 +566,8 @@ namespace Esim.Apis.Business
                 otpRecord.IsUsed = true;
 
                 // Get customer info
-                var customer = dbContext.CustomerInfos
-                    .Where(c => c.Email == request.email)
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == request.email)
                     .FirstOrDefault();
 
                 if (customer == null)
@@ -547,14 +591,21 @@ namespace Esim.Apis.Business
                 int tokenExpireHours = 24;
                 int refreshTokenExpireDays = 30;
 
-                string accessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
-                string refreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
+                string accessToken = CommonLogic.GenToken(
+                    configuration,
+                    customer.Email ?? "",
+                    customer.Id.ToString() ?? ""
+                );
+                string refreshToken = CommonLogic.GenRefreshToken(
+                    configuration,
+                    customer.Email ?? ""
+                );
                 var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
                 var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
 
                 // Revoke old tokens
-                var oldTokens = dbContext.UserTokens
-                    .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
+                var oldTokens = dbContext
+                    .UserTokens.Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
                     .ToList();
 
                 foreach (var oldToken in oldTokens)
@@ -642,23 +693,26 @@ namespace Esim.Apis.Business
             return ipAddress ?? "Unknown";
         }
 
-        public async Task<IActionResult> GoogleLogin(HttpRequest httpRequest, GoogleLoginReq request)
+        public async Task<IActionResult> GoogleLogin(
+            HttpRequest httpRequest,
+            GoogleLoginReq request
+        )
         {
             var url = httpRequest.Path;
             var json = JsonConvert.SerializeObject(request);
             log.Debug("URL: " + url + " => GoogleLogin Request: " + json);
-            
-            try 
+
+            try
             {
                 string lang = CommonLogic.GetLanguage(httpRequest, request?.lang);
-                
+
                 var clientId = configuration["Google:ClientId"];
                 var redirectUri = configuration["Google:RedirectUri"];
-                
+
                 if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(redirectUri))
                 {
                     log.Error("Google Auth configuration missing");
-                     return DotnetLib.Http.HttpResponse.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         url,
                         json,
@@ -668,21 +722,22 @@ namespace Esim.Apis.Business
                     );
                 }
 
-                var googleUrl = $"https://accounts.google.com/o/oauth2/v2/auth?client_id={clientId}&redirect_uri={redirectUri}&response_type=code&scope=email%20profile";
-                
+                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";
+
                 return DotnetLib.Http.HttpResponse.BuildResponse(
-                        log,
-                        url,
-                        json,
-                        CommonErrorCode.Success,
-                        ConfigManager.Instance.GetConfigWebValue("SUCCESS", lang),
-                        new { url = googleUrl }
-                    );
+                    log,
+                    url,
+                    json,
+                    CommonErrorCode.Success,
+                    ConfigManager.Instance.GetConfigWebValue("SUCCESS", lang),
+                    new { url = googleUrl }
+                );
             }
             catch (Exception ex)
             {
-                 log.Error("GoogleLogin Exception: ", ex);
-                 return DotnetLib.Http.HttpResponse.BuildResponse(
+                log.Error("GoogleLogin Exception: ", ex);
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     url,
                     json,
@@ -693,20 +748,23 @@ namespace Esim.Apis.Business
             }
         }
 
-        public async Task<IActionResult> GoogleCallback(HttpRequest httpRequest, GoogleCallbackReq request)
+        public async Task<IActionResult> GoogleCallback(
+            HttpRequest httpRequest,
+            GoogleCallbackReq request
+        )
         {
             var url = httpRequest.Path;
             var json = JsonConvert.SerializeObject(request);
             log.Debug("URL: " + url + " => GoogleCallback Request: " + json);
-            
+
             try
             {
                 // Get language for response messages
                 string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
-                
+
                 if (string.IsNullOrEmpty(request.code))
                 {
-                     return DotnetLib.Http.HttpResponse.BuildResponse(
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
                         log,
                         url,
                         json,
@@ -718,32 +776,42 @@ namespace Esim.Apis.Business
 
                 var clientId = configuration["Google:ClientId"];
                 var clientSecret = configuration["Google:ClientSecret"];
-                var redirectUri = !string.IsNullOrEmpty(request.redirectUri) ? request.redirectUri : configuration["Google:RedirectUri"];
+                var redirectUri = !string.IsNullOrEmpty(request.redirectUri)
+                    ? request.redirectUri
+                    : configuration["Google:RedirectUri"];
 
                 using (var httpClient = new HttpClient())
                 {
                     // 1. Exchange code for token
-                    var tokenRequestContent = new FormUrlEncodedContent(new[]
-                    {
-                        new KeyValuePair<string, string>("code", request.code),
-                        new KeyValuePair<string, string>("client_id", clientId),
-                        new KeyValuePair<string, string>("client_secret", clientSecret),
-                        new KeyValuePair<string, string>("redirect_uri", redirectUri),
-                        new KeyValuePair<string, string>("grant_type", "authorization_code")
-                    });
-
-                    var tokenResponse = await httpClient.PostAsync("https://oauth2.googleapis.com/token", tokenRequestContent);
+                    var tokenRequestContent = new FormUrlEncodedContent(
+                        new[]
+                        {
+                            new KeyValuePair<string, string>("code", request.code),
+                            new KeyValuePair<string, string>("client_id", clientId),
+                            new KeyValuePair<string, string>("client_secret", clientSecret),
+                            new KeyValuePair<string, string>("redirect_uri", redirectUri),
+                            new KeyValuePair<string, string>("grant_type", "authorization_code")
+                        }
+                    );
+
+                    var tokenResponse = await httpClient.PostAsync(
+                        "https://oauth2.googleapis.com/token",
+                        tokenRequestContent
+                    );
                     var tokenResponseString = await tokenResponse.Content.ReadAsStringAsync();
-                    
+
                     if (!tokenResponse.IsSuccessStatusCode)
                     {
-                         log.Error($"Google Token Exchange Failed: {tokenResponseString}");
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log.Error($"Google Token Exchange Failed: {tokenResponseString}");
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.ExternalServiceError,
-                            ConfigManager.Instance.GetConfigWebValue("GOOGLE_TOKEN_EXCHANGE_FAILED", lang),
+                            ConfigManager.Instance.GetConfigWebValue(
+                                "GOOGLE_TOKEN_EXCHANGE_FAILED",
+                                lang
+                            ),
                             new { error = tokenResponseString }
                         );
                     }
@@ -753,30 +821,41 @@ namespace Esim.Apis.Business
 
                     if (string.IsNullOrEmpty(accessToken))
                     {
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.ExternalServiceError,
-                            ConfigManager.Instance.GetConfigWebValue("GOOGLE_NO_ACCESS_TOKEN", lang),
+                            ConfigManager.Instance.GetConfigWebValue(
+                                "GOOGLE_NO_ACCESS_TOKEN",
+                                lang
+                            ),
                             new { }
                         );
                     }
 
                     // 2. Get User Info
-                    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
-                    var userInfoResponse = await httpClient.GetAsync("https://www.googleapis.com/oauth2/v2/userinfo");
+                    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
+                        "Bearer",
+                        accessToken
+                    );
+                    var userInfoResponse = await httpClient.GetAsync(
+                        "https://www.googleapis.com/oauth2/v2/userinfo"
+                    );
                     var userInfoString = await userInfoResponse.Content.ReadAsStringAsync();
 
                     if (!userInfoResponse.IsSuccessStatusCode)
                     {
-                         log.Error($"Google User Info Failed: {userInfoString}");
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log.Error($"Google User Info Failed: {userInfoString}");
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
                             CommonErrorCode.ExternalServiceError,
-                            ConfigManager.Instance.GetConfigWebValue("GOOGLE_USERINFO_FAILED", lang),
+                            ConfigManager.Instance.GetConfigWebValue(
+                                "GOOGLE_USERINFO_FAILED",
+                                lang
+                            ),
                             new { error = userInfoString }
                         );
                     }
@@ -785,10 +864,10 @@ namespace Esim.Apis.Business
                     var email = userInfo["email"]?.ToString();
                     var name = userInfo["name"]?.ToString(); // Full name
                     var picture = userInfo["picture"]?.ToString();
-                    
+
                     if (string.IsNullOrEmpty(email))
                     {
-                         return DotnetLib.Http.HttpResponse.BuildResponse(
+                        return DotnetLib.Http.HttpResponse.BuildResponse(
                             log,
                             url,
                             json,
@@ -799,14 +878,16 @@ namespace Esim.Apis.Business
                     }
 
                     // 3. Login or Register logic
-                     var customer = dbContext.CustomerInfos
-                        .FirstOrDefault(c => c.Email == email);
+                    var customer = dbContext.CustomerInfos.FirstOrDefault(c => c.Email == email);
 
                     if (customer == null)
                     {
                         // Register new user
-                        var newCustomerId = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ");
-                        
+                        var newCustomerId = await Database.DbLogic.GenIdAsync(
+                            dbContext,
+                            "CUSTOMER_INFO_SEQ"
+                        );
+
                         // Try to split name
                         string surname = name;
                         string lastname = "";
@@ -819,9 +900,9 @@ namespace Esim.Apis.Business
                                 surname = string.Join(" ", parts.Take(parts.Length - 1));
                             }
                         }
-                        else 
+                        else
                         {
-                             surname = email.Split('@')[0];
+                            surname = email.Split('@')[0];
                         }
 
                         customer = new CustomerInfo
@@ -830,7 +911,7 @@ namespace Esim.Apis.Business
                             Email = email,
                             SurName = surname,
                             LastName = lastname,
-                            AvatarUrl = picture, 
+                            AvatarUrl = picture,
                             Status = true,
                             IsVerified = true, // Verified by Google
                             CreatedDate = DateTime.Now,
@@ -838,34 +919,48 @@ namespace Esim.Apis.Business
                         };
                         dbContext.CustomerInfos.Add(customer);
                         await dbContext.SaveChangesAsync();
-                        log.Info($"Created new customer via Google Login: ID={newCustomerId}, Email={email}");
+                        log.Info(
+                            $"Created new customer via Google Login: ID={newCustomerId}, Email={email}"
+                        );
                     }
-                    else 
+                    else
                     {
                         // Update existing user info if needed or just log them in
-                        customer.IsVerified = true; 
-                        if (string.IsNullOrEmpty(customer.AvatarUrl) && !string.IsNullOrEmpty(picture))
+                        customer.IsVerified = true;
+                        if (
+                            string.IsNullOrEmpty(customer.AvatarUrl)
+                            && !string.IsNullOrEmpty(picture)
+                        )
                         {
                             customer.AvatarUrl = picture;
                         }
                         customer.LastLoginDate = DateTime.Now;
                         customer.LastUpdate = DateTime.Now;
                         await dbContext.SaveChangesAsync();
-                        log.Info($"Existing customer logged in via Google: ID={customer.Id}, Email={email}");
+                        log.Info(
+                            $"Existing customer logged in via Google: ID={customer.Id}, Email={email}"
+                        );
                     }
 
                     // 4. Generate JWT
                     int tokenExpireHours = 24;
                     int refreshTokenExpireDays = 30;
 
-                    string jwtAccessToken = CommonLogic.GenToken(configuration, customer.Email ?? "", customer.Id.ToString() ?? "");
-                    string jwtRefreshToken = CommonLogic.GenRefreshToken(configuration, customer.Email ?? "");
+                    string jwtAccessToken = CommonLogic.GenToken(
+                        configuration,
+                        customer.Email ?? "",
+                        customer.Id.ToString() ?? ""
+                    );
+                    string jwtRefreshToken = CommonLogic.GenRefreshToken(
+                        configuration,
+                        customer.Email ?? ""
+                    );
                     var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
                     var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
 
                     // Revoke old tokens
-                    var oldTokens = dbContext.UserTokens
-                        .Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
+                    var oldTokens = dbContext
+                        .UserTokens.Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
                         .ToList();
 
                     foreach (var oldToken in oldTokens)
@@ -874,7 +969,8 @@ namespace Esim.Apis.Business
                     }
 
                     // Save new token
-                    var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
+                    var tokenId = (int)
+                        await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
                     var userToken = new UserToken
                     {
                         Id = tokenId,
@@ -916,7 +1012,7 @@ namespace Esim.Apis.Business
             catch (Exception exception)
             {
                 log.Error("GoogleCallback Exception: ", exception);
-                 return DotnetLib.Http.HttpResponse.BuildResponse(
+                return DotnetLib.Http.HttpResponse.BuildResponse(
                     log,
                     url,
                     json,
@@ -926,6 +1022,5 @@ namespace Esim.Apis.Business
                 );
             }
         }
-
     }
 }

+ 5 - 3
EsimLao/Esim.Apis/Controllers/UserController.cs

@@ -2,8 +2,8 @@ using System;
 using Common;
 using Common.Constant;
 using Common.Http;
-using Esim.Apis.Business;
 using Database.Database;
+using Esim.Apis.Business;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 
@@ -81,7 +81,10 @@ namespace RevoSystem.Apis.Controllers
         [Route(ApiUrlConstant.GoogleLoginUrl)]
         public async Task<IActionResult> GoogleLogin([FromBody] GoogleLoginReq request)
         {
-            return await userBusiness.GoogleLogin(HttpContext.Request, request ?? new GoogleLoginReq());
+            return await userBusiness.GoogleLogin(
+                HttpContext.Request,
+                request ?? new GoogleLoginReq()
+            );
         }
 
         /// <summary>
@@ -97,6 +100,5 @@ namespace RevoSystem.Apis.Controllers
         {
             return await userBusiness.GoogleCallback(HttpContext.Request, request);
         }
-
     }
 }

+ 3 - 3
EsimLao/Esim.Apis/Esim.Apis.csproj

@@ -23,12 +23,12 @@
 		<PackageReference Include="NCrontab" Version="3.3.3" />
 	</ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="..\Common\Common.csproj" />
-    <ProjectReference Include="..\Database\Database.csproj" />
+    <ProjectReference Include="../Common/Common.csproj" />
+    <ProjectReference Include="../Database/Database.csproj" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="DotnetLib">
-      <HintPath>..\..\..\..\lib\DotnetLib.dll</HintPath>
+      <HintPath>../lib/DotnetLib.dll</HintPath>
     </Reference>
   </ItemGroup>
 

+ 56 - 46
EsimLao/Esim.Apis/Program.cs

@@ -2,33 +2,38 @@ using System.Text;
 using Common.Global;
 using Database.Database;
 using Esim.Apis.Business;
+using log4net;
+using log4net.Config;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.IdentityModel.Tokens;
 
-using log4net;
-using log4net.Config;
-
 var builder = WebApplication.CreateBuilder(args);
 
 // Initialize log4net
-XmlConfigurator.Configure(new FileInfo(Path.Combine(builder.Environment.ContentRootPath, "log4net.config")));
+XmlConfigurator.Configure(
+    new FileInfo(Path.Combine(builder.Environment.ContentRootPath, "log4net.config"))
+);
 
 // Set global configuration for use by singletons like ConfigManager
 GlobalConfig.Configuration = builder.Configuration;
 
 // Add services to the container.
-builder.Services.AddControllersWithViews()
+builder
+    .Services.AddControllersWithViews()
     .AddJsonOptions(options =>
     {
         // Use camelCase for JSON property names (categoryId instead of CategoryId)
-        options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
+        options.JsonSerializerOptions.PropertyNamingPolicy = System
+            .Text
+            .Json
+            .JsonNamingPolicy
+            .CamelCase;
     });
 
 // Add DbContext with Oracle provider
 var connectionString = builder.Configuration.GetSection("Connection").Value;
-builder.Services.AddDbContext<ModelContext>(options =>
-    options.UseOracle(connectionString));
+builder.Services.AddDbContext<ModelContext>(options => options.UseOracle(connectionString));
 
 // Add Business Services
 builder.Services.AddScoped<IUserBusiness, UserBusinessImpl>();
@@ -38,46 +43,53 @@ builder.Services.AddScoped<IContentBusiness, ContentBusinessImpl>();
 // Configure CORS - Allow frontend to access APIs
 builder.Services.AddCors(options =>
 {
-    options.AddPolicy("AllowFrontend", policy =>
-    {
-        policy.WithOrigins(
-                "http://localhost:3000",      // React development
-                "http://localhost:5173",      // Vite development
-                "http://localhost:4200",      // Angular development
-                "http://simgetgo.vn",        // Production domain
-                "http://simgetgo.com",     // Production www domain
-                "https://simgetgo.vn",    // Production www domain
-                "https://simgetgo.com"     // Production www domain
-            )
-            .AllowAnyMethod()
-            .AllowAnyHeader()
-            .AllowCredentials();
-    });
+    options.AddPolicy(
+        "AllowFrontend",
+        policy =>
+        {
+            policy
+                .WithOrigins(
+                    "http://localhost:3000", // React development
+                    "http://localhost:5173", // Vite development
+                    "http://localhost:4200", // Angular development
+                    "http://simgetgo.vn", // Production domain
+                    "http://simgetgo.com", // Production www domain
+                    "https://simgetgo.vn", // Production www domain
+                    "https://simgetgo.com" // Production www domain
+                )
+                .AllowAnyMethod()
+                .AllowAnyHeader()
+                .AllowCredentials();
+        }
+    );
 });
 
 // Configure JWT Authentication
-var jwtKey = builder.Configuration["Jwt:Key"] ?? "EsimLaoSecretKey1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD";
+var jwtKey =
+    builder.Configuration["Jwt:Key"]
+    ?? "EsimLaoSecretKey1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD";
 var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "EsimLao";
 var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "EsimLaoClient";
 
-builder.Services.AddAuthentication(options =>
-{
-    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
-    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
-})
-.AddJwtBearer(options =>
-{
-    options.TokenValidationParameters = new TokenValidationParameters
+builder
+    .Services.AddAuthentication(options =>
     {
-        ValidateIssuer = true,
-        ValidateAudience = true,
-        ValidateLifetime = true,
-        ValidateIssuerSigningKey = true,
-        ValidIssuer = jwtIssuer,
-        ValidAudience = jwtAudience,
-        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtKey))
-    };
-});
+        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+    })
+    .AddJwtBearer(options =>
+    {
+        options.TokenValidationParameters = new TokenValidationParameters
+        {
+            ValidateIssuer = true,
+            ValidateAudience = true,
+            ValidateLifetime = true,
+            ValidateIssuerSigningKey = true,
+            ValidIssuer = jwtIssuer,
+            ValidAudience = jwtAudience,
+            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtKey))
+        };
+    });
 
 // Add Swagger for API documentation
 builder.Services.AddEndpointsApiExplorer();
@@ -94,7 +106,7 @@ using (var scope = app.Services.CreateScope())
         logger.LogInformation("Initializing ConfigManager...");
         Esim.Apis.Singleton.ConfigManager.Instance.Initialize();
         logger.LogInformation("ConfigManager initialized successfully");
-        
+
         // Start background refresh task
         Task.Run(() => Esim.Apis.Singleton.ConfigManager.Instance.RefreshConfigs());
     }
@@ -105,6 +117,7 @@ using (var scope = app.Services.CreateScope())
 }
 app.UseSwagger();
 app.UseSwaggerUI();
+
 // Configure the HTTP request pipeline.
 //if (app.Environment.IsDevelopment())
 //{
@@ -134,12 +147,9 @@ app.UseAuthorization();
 
 app.MapStaticAssets();
 
-app.MapControllerRoute(
-    name: "default",
-    pattern: "{controller=Home}/{action=Index}/{id?}")
+app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}")
     .WithStaticAssets();
 
 app.MapControllers();
 
 app.Run();
-

+ 1 - 2
EsimLao/Esim.Apis/appsettings.json

@@ -31,5 +31,4 @@
     "ClientSecret": "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",
     "RedirectUri": "https://simgetgo.vn"
   }
-
-}
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 11 - 13
EsimLao/docs/.$skyhub.drawio.bkp


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 11 - 13
EsimLao/docs/skyhub.drawio


BIN
EsimLao/lib/DotnetLib.dll


Vissa filer visades inte eftersom för många filer har ändrats