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 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
{
public class UserBusinessImpl : IUserBusiness
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(
typeof(UserBusinessImpl)
);
private ModelContext dbContext;
IConfiguration configuration;
public UserBusinessImpl(ModelContext _dbContext, IConfiguration _configuration)
{
dbContext = _dbContext;
configuration = _configuration;
}
private string GetParameter(string key)
{
return configuration.GetSection(key).Value ?? "";
}
///
/// Request OTP to be sent to email
///
public async Task RequestOtp(HttpRequest httpRequest, RequestOtpReq request)
{
var url = httpRequest.Path;
var json = JsonConvert.SerializeObject(request);
log.Debug("URL: " + url + " => Request: " + json);
try
{
if (string.IsNullOrEmpty(request.email))
{
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.RequiredFieldMissing,
ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED"),
new { }
);
}
// Generate 6-digit OTP (fixed 111111 for test account abc@gmail.com)
bool isTestAccount = request.email.ToLower() == "abc@gmail.com";
string otpCode = isTestAccount ? "111111" : GenerateOtp();
// Check if customer exists, if not create new
var customer = dbContext.CustomerInfos
.Where(c => c.Email == request.email)
.FirstOrDefault();
decimal? customerId = customer?.Id;
if (customer == null)
{
// Create new customer record - manually get ID from Oracle sequence
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,
Email = request.email,
SurName = emailUsername,
LastName = emailUsername,
Status = true,
IsVerified = false,
CreatedDate = DateTime.Now,
LastUpdate = DateTime.Now
};
dbContext.CustomerInfos.Add(newCustomer);
await dbContext.SaveChangesAsync();
customerId = newCustomerId;
}
// 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)
.OrderByDescending(o => o.CreatedDate)
.FirstOrDefault();
if (existingOtp != null)
{
// Anti-spam check: prevent rapid OTP requests
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.");
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.OtpTooManyRequests,
$"Please wait {waitSeconds} seconds before requesting a new OTP.",
new {
waitSeconds = waitSeconds,
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.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")})");
}
else
{
// INSERT new record only for first-time email
var otpId = (int)await Database.DbLogic.GenIdAsync(dbContext, "OTP_VERIFICATION_SEQ");
var otpVerification = new OtpVerification
{
Id = otpId,
CustomerId = customerId,
UserEmail = request.email,
OtpCode = otpCode,
OtpType = 1, // Login OTP
ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes),
IsUsed = false,
AttemptCount = 0,
CreatedDate = DateTime.Now
};
dbContext.OtpVerifications.Add(otpVerification);
log.Info($"Created new OTP record for {request.email} (first time)");
}
await dbContext.SaveChangesAsync();
// Skip email sending for test account
if (!isTestAccount)
{
// Add to MESSAGE_QUEUE for background email sending
// Resolve template content now so Worker only needs to send email
string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
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);
if (template == null)
{
log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE");
throw new Exception($"Email template '{templateCode}' not found");
}
// 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 ?? "");
// 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 ?? "");
// Replace placeholders in content
emailContent = emailContent
.Replace("{{OTP_CODE}}", otpCode)
.Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
// Replace placeholders in subject (if any)
emailSubject = emailSubject
.Replace("{{OTP_CODE}}", otpCode)
.Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
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
Priority = true, // High priority
Status = 0, // Pending
ScheduledAt = DateTime.Now,
RetryCount = 0,
MaxRetry = 3,
CreatedBy = customerId,
CreatedDate = DateTime.Now
};
dbContext.MessageQueues.Add(emailMessage);
await dbContext.SaveChangesAsync();
}
log.Info($"OTP generated for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
//return DotnetLib.Http.HttpResponse.BuildResponse(
// log,
// url,
// json,
// CommonErrorCode.Success,
// ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
// new
// {
// email = request.email,
// expireInSeconds = otpExpireMinutes * 60
// }
//);
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.Success,
ConfigManager.Instance.GetConfigWebValue("OTP_SENT_SUCCESS"),
new
{
email = request.email,
expireInSeconds = otpExpireMinutes * 60
}
);
}
catch (Exception exception)
{
log.Error("Exception: ", exception);
}
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.SystemError,
ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
new { }
);
}
///
/// Resend OTP - Only works if user has already requested OTP before
/// Has reduced cooldown (30 seconds vs 60 seconds for RequestOtp)
///
public async Task ResendOtp(HttpRequest httpRequest, RequestOtpReq request)
{
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))
{
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.RequiredFieldMissing,
ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED", lang),
new { }
);
}
// Check if there's an existing OTP record for this email
var existingOtp = dbContext.OtpVerifications
.Where(o => o.UserEmail == request.email)
.OrderByDescending(o => o.CreatedDate)
.FirstOrDefault();
// RESEND requires existing OTP - must have requested OTP before
if (existingOtp == null)
{
log.Warn($"ResendOtp failed: No existing OTP record for {request.email}");
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.OtpNotRequested,
ConfigManager.Instance.GetConfigWebValue("OTP_NOT_REQUESTED", lang),
new { }
);
}
// RESEND has reduced cooldown: 60 seconds (vs 60 seconds for RequestOtp)
int minSecondsBetweenResend = 60;
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.");
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.OtpTooManyRequests,
$"Please wait {waitSeconds} seconds before resending OTP.",
new {
waitSeconds = waitSeconds,
canResendAt = existingOtp.CreatedDate?.AddSeconds(minSecondsBetweenResend)
}
);
}
// 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)
.FirstOrDefault();
decimal? customerId = customer?.Id ?? existingOtp.CustomerId;
// UPDATE existing OTP record with new code and expiry
existingOtp.OtpCode = otpCode;
existingOtp.ExpiredAt = DateTime.Now.AddMinutes(otpExpireMinutes);
existingOtp.AttemptCount = 0; // Reset attempt count
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
if (!isTestAccount)
{
// 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);
if (template == null)
{
log.Error($"Template '{templateCode}' not found in MESSAGE_TEMPLATE");
throw new Exception($"Email template '{templateCode}' not found");
}
// Get subject based on language
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 ?? "");
// Replace placeholders
emailContent = emailContent
.Replace("{{OTP_CODE}}", otpCode)
.Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
emailSubject = emailSubject
.Replace("{{OTP_CODE}}", otpCode)
.Replace("{{EXPIRE_MINUTES}}", otpExpireMinutes.ToString());
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,
Content = emailContent,
Priority = true, // High priority
Status = 0, // Pending
ScheduledAt = DateTime.Now,
RetryCount = 0,
MaxRetry = 3,
CreatedBy = customerId,
CreatedDate = DateTime.Now
};
dbContext.MessageQueues.Add(emailMessage);
await dbContext.SaveChangesAsync();
}
log.Info($"ResendOtp: OTP resent for {request.email}: {otpCode} - {(isTestAccount ? "Test account, no email sent" : "Email queued")}");
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.Success,
ConfigManager.Instance.GetConfigWebValue("OTP_RESENT_SUCCESS", lang),
new
{
email = request.email,
expireInSeconds = otpExpireMinutes * 60
}
);
}
catch (Exception exception)
{
log.Error("ResendOtp Exception: ", exception);
}
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.SystemError,
ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
new { }
);
}
///
/// Verify OTP and complete login - return JWT token
///
public async Task VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request)
{
var url = httpRequest.Path;
var json = JsonConvert.SerializeObject(request);
log.Debug("URL: " + url + " => Request: " + json);
try
{
if (string.IsNullOrEmpty(request.email) || string.IsNullOrEmpty(request.otpCode))
{
string lang = CommonLogic.GetLanguage(httpRequest, request.lang);
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.RequiredFieldMissing,
ConfigManager.Instance.GetConfigWebValue("EMAIL_OTP_REQUIRED", lang),
new { }
);
}
// Get language for response messages
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)
.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)
.FirstOrDefault();
if (anyOtp != null)
{
if (anyOtp.IsUsed == true)
{
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.OtpAlreadyUsed,
ConfigManager.Instance.GetConfigWebValue("OTP_ALREADY_USED", responseLang),
new { }
);
}
if (anyOtp.ExpiredAt <= DateTime.Now)
{
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.OtpExpired,
ConfigManager.Instance.GetConfigWebValue("OTP_EXPIRED", responseLang),
new { }
);
}
}
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.OtpInvalid,
ConfigManager.Instance.GetConfigWebValue("OTP_INVALID", responseLang),
new { }
);
}
// Mark OTP as used
otpRecord.IsUsed = true;
// Get customer info
var customer = dbContext.CustomerInfos
.Where(c => c.Email == request.email)
.FirstOrDefault();
if (customer == null)
{
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.UserNotFound,
ConfigManager.Instance.GetConfigWebValue("USER_NOT_FOUND", responseLang),
new { }
);
}
// Update customer verification status
customer.IsVerified = true;
customer.LastLoginDate = DateTime.Now;
customer.LastUpdate = DateTime.Now;
// Generate JWT tokens
int tokenExpireHours = 24;
int refreshTokenExpireDays = 30;
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)
.ToList();
foreach (var oldToken in oldTokens)
{
oldToken.IsRevoked = true;
}
// Save new token
var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
var userToken = new UserToken
{
Id = tokenId,
CustomerId = customer.Id,
AccessToken = accessToken,
RefreshToken = refreshToken,
TokenType = "Bearer",
DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
IpAddress = GetClientIpAddress(httpRequest),
ExpiredAt = expiresAt,
RefreshExpiredAt = refreshExpiresAt,
IsRevoked = false,
CreatedDate = DateTime.Now,
LastUsed = DateTime.Now
};
dbContext.UserTokens.Add(userToken);
await dbContext.SaveChangesAsync();
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.Success,
ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", responseLang),
new
{
userId = customer.Id,
email = customer.Email ?? "",
fullName = $"{customer.SurName} {customer.LastName}".Trim(),
avatarUrl = customer.AvatarUrl,
accessToken,
refreshToken,
expiresAt
}
);
}
catch (Exception exception)
{
log.Error("Exception: ", exception);
}
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.SystemError,
ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
new { }
);
}
///
/// Generate 6-digit OTP code
///
private string GenerateOtp()
{
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
{
var bytes = new byte[4];
rng.GetBytes(bytes);
var number = Math.Abs(BitConverter.ToInt32(bytes, 0)) % 1000000;
return number.ToString("D6");
}
}
///
/// Get client IP address
///
private string GetClientIpAddress(HttpRequest httpRequest)
{
var ipAddress = httpRequest.Headers["X-Forwarded-For"].FirstOrDefault();
if (string.IsNullOrEmpty(ipAddress))
{
ipAddress = httpRequest.HttpContext.Connection.RemoteIpAddress?.ToString();
}
return ipAddress ?? "Unknown";
}
public async Task GoogleLogin(HttpRequest httpRequest, GoogleLoginReq request)
{
var url = httpRequest.Path;
var json = JsonConvert.SerializeObject(request);
log.Debug("URL: " + url + " => GoogleLogin Request: " + json);
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(
log,
url,
json,
CommonErrorCode.SystemError,
ConfigManager.Instance.GetConfigWebValue("GOOGLE_CONFIG_MISSING", lang),
new { }
);
}
var googleUrl = $"https://accounts.google.com/o/oauth2/v2/auth?client_id={clientId}&redirect_uri={redirectUri}&response_type=code&scope=email%20profile";
return DotnetLib.Http.HttpResponse.BuildResponse(
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,
url,
json,
CommonErrorCode.SystemError,
ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
new { }
);
}
}
public async Task 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(
log,
url,
json,
CommonErrorCode.RequiredFieldMissing,
ConfigManager.Instance.GetConfigWebValue("GOOGLE_CODE_REQUIRED", lang),
new { }
);
}
var clientId = configuration["Google:ClientId"];
var clientSecret = configuration["Google:ClientSecret"];
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("code", request.code),
new KeyValuePair("client_id", clientId),
new KeyValuePair("client_secret", clientSecret),
new KeyValuePair("redirect_uri", redirectUri),
new KeyValuePair("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,
url,
json,
CommonErrorCode.ExternalServiceError,
ConfigManager.Instance.GetConfigWebValue("GOOGLE_TOKEN_EXCHANGE_FAILED", lang),
new { error = tokenResponseString }
);
}
var tokenData = JsonConvert.DeserializeObject(tokenResponseString);
var accessToken = tokenData["access_token"]?.ToString();
if (string.IsNullOrEmpty(accessToken))
{
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.ExternalServiceError,
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");
var userInfoString = await userInfoResponse.Content.ReadAsStringAsync();
if (!userInfoResponse.IsSuccessStatusCode)
{
log.Error($"Google User Info Failed: {userInfoString}");
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.ExternalServiceError,
ConfigManager.Instance.GetConfigWebValue("GOOGLE_USERINFO_FAILED", lang),
new { error = userInfoString }
);
}
var userInfo = JsonConvert.DeserializeObject(userInfoString);
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(
log,
url,
json,
CommonErrorCode.ExternalServiceError,
ConfigManager.Instance.GetConfigWebValue("GOOGLE_NO_EMAIL", lang),
new { }
);
}
// 3. Login or Register logic
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");
// Try to split name
string surname = name;
string lastname = "";
if (!string.IsNullOrEmpty(name))
{
var parts = name.Split(' ');
if (parts.Length > 1)
{
lastname = parts[parts.Length - 1];
surname = string.Join(" ", parts.Take(parts.Length - 1));
}
}
else
{
surname = email.Split('@')[0];
}
customer = new CustomerInfo
{
Id = newCustomerId,
Email = email,
SurName = surname,
LastName = lastname,
AvatarUrl = picture,
Status = true,
IsVerified = true, // Verified by Google
CreatedDate = DateTime.Now,
LastUpdate = DateTime.Now
};
dbContext.CustomerInfos.Add(customer);
await dbContext.SaveChangesAsync();
log.Info($"Created new customer via Google Login: ID={newCustomerId}, Email={email}");
}
else
{
// Update existing user info if needed or just log them in
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}");
}
// 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 ?? "");
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)
.ToList();
foreach (var oldToken in oldTokens)
{
oldToken.IsRevoked = true;
}
// Save new token
var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
var userToken = new UserToken
{
Id = tokenId,
CustomerId = customer.Id,
AccessToken = jwtAccessToken,
RefreshToken = jwtRefreshToken,
TokenType = "Bearer",
DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
IpAddress = GetClientIpAddress(httpRequest),
ExpiredAt = expiresAt,
RefreshExpiredAt = refreshExpiresAt,
IsRevoked = false,
CreatedDate = DateTime.Now,
LastUsed = DateTime.Now
};
dbContext.UserTokens.Add(userToken);
await dbContext.SaveChangesAsync();
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.Success,
ConfigManager.Instance.GetConfigWebValue("GOOGLE_LOGIN_SUCCESS", lang),
new
{
userId = customer.Id,
email = customer.Email ?? "",
fullName = $"{customer.SurName} {customer.LastName}".Trim(),
avatarUrl = customer.AvatarUrl,
accessToken = jwtAccessToken,
refreshToken = jwtRefreshToken,
expiresAt
}
);
}
}
catch (Exception exception)
{
log.Error("GoogleCallback Exception: ", exception);
return DotnetLib.Http.HttpResponse.BuildResponse(
log,
url,
json,
CommonErrorCode.SystemError,
ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
new { }
);
}
}
}
}