feat: ajustes do stripe
This commit is contained in:
parent
0c176a2abf
commit
3fa95aefd8
@ -112,13 +112,21 @@ namespace QRRapidoApp.Controllers
|
|||||||
_logger.LogInformation("QR code generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, Size: {Size}px",
|
_logger.LogInformation("QR code generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, Size: {Size}px",
|
||||||
generationStopwatch.ElapsedMilliseconds, result.FromCache, request.Size);
|
generationStopwatch.ElapsedMilliseconds, result.FromCache, request.Size);
|
||||||
|
|
||||||
// Update counter for free users
|
// Update counter for all logged users
|
||||||
if (!request.IsPremium && userId != null)
|
if (userId != null)
|
||||||
{
|
{
|
||||||
var remaining = await _userService.DecrementDailyQRCountAsync(userId);
|
if (request.IsPremium)
|
||||||
result.RemainingQRs = remaining;
|
{
|
||||||
|
result.RemainingQRs = int.MaxValue; // Premium users have unlimited
|
||||||
_logger.LogDebug("Updated QR count for free user - Remaining: {RemainingQRs}", remaining);
|
// Still increment the count for statistics
|
||||||
|
await _userService.IncrementDailyQRCountAsync(userId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var remaining = await _userService.IncrementDailyQRCountAsync(userId);
|
||||||
|
result.RemainingQRs = remaining;
|
||||||
|
_logger.LogDebug("Updated QR count for free user - Remaining: {RemainingQRs}", remaining);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to history if user is logged in (fire and forget)
|
// Save to history if user is logged in (fire and forget)
|
||||||
@ -467,11 +475,48 @@ namespace QRRapidoApp.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("GetUserStats")]
|
||||||
|
public async Task<IActionResult> GetUserStats()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
var isPremium = user?.IsPremium ?? false;
|
||||||
|
|
||||||
|
// For logged users (premium or not), return -1 to indicate unlimited
|
||||||
|
// For consistency with the frontend logic
|
||||||
|
var remainingCount = -1; // Unlimited for all logged users
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
remainingCount = remainingCount,
|
||||||
|
isPremium = isPremium,
|
||||||
|
isUnlimited = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting user stats");
|
||||||
|
return StatusCode(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
|
private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
|
||||||
{
|
{
|
||||||
|
// Premium users have unlimited QR codes
|
||||||
if (user?.IsPremium == true) return true;
|
if (user?.IsPremium == true) return true;
|
||||||
|
|
||||||
var dailyLimit = userId != null ? 50 : 10; // Logged in: 50/day, Anonymous: 10/day
|
// Logged users (non-premium) have unlimited QR codes
|
||||||
|
if (userId != null) return true;
|
||||||
|
|
||||||
|
// Anonymous users have 3 QR codes per day
|
||||||
|
var dailyLimit = 3;
|
||||||
var currentCount = await _userService.GetDailyQRCountAsync(userId);
|
var currentCount = await _userService.GetDailyQRCountAsync(userId);
|
||||||
|
|
||||||
return currentCount < dailyLimit;
|
return currentCount < dailyLimit;
|
||||||
|
|||||||
@ -67,7 +67,8 @@ namespace QRRapidoApp.Middleware
|
|||||||
"api/", "health", "_framework/", "lib/", "css/", "js/", "images/",
|
"api/", "health", "_framework/", "lib/", "css/", "js/", "images/",
|
||||||
"favicon.ico", "robots.txt", "sitemap.xml",
|
"favicon.ico", "robots.txt", "sitemap.xml",
|
||||||
"signin-microsoft", "signin-google", "signout-callback-oidc",
|
"signin-microsoft", "signin-google", "signout-callback-oidc",
|
||||||
"Account/ExternalLoginCallback", "Account/Logout"
|
"Account/ExternalLoginCallback", "Account/Logout", "Pagamento/CreateCheckout",
|
||||||
|
"Pagamento/StripeWebhook"
|
||||||
};
|
};
|
||||||
|
|
||||||
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));
|
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
|
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" />
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||||
<PackageReference Include="Stripe.net" Version="43.15.0" />
|
<PackageReference Include="Stripe.net" Version="48.4.0" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.7.4" />
|
<PackageReference Include="StackExchange.Redis" Version="2.7.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||||
|
|||||||
@ -343,3 +343,7 @@ Este projeto está licenciado sob a licença MIT - veja o arquivo [LICENSE](LICE
|
|||||||
**QR Rapido** - Velocidade extrema meets experiência excepcional 🚀
|
**QR Rapido** - Velocidade extrema meets experiência excepcional 🚀
|
||||||
|
|
||||||
*Desenvolvido com ❤️ para a comunidade de desenvolvedores*
|
*Desenvolvido com ❤️ para a comunidade de desenvolvedores*
|
||||||
|
|
||||||
|
No WSL - Para rodar o webhook do stripe local.
|
||||||
|
```bash
|
||||||
|
stripe listen --forward-to https://localhost:52428/pagamento/stripewebhook --skip-verify
|
||||||
|
|||||||
@ -774,4 +774,13 @@
|
|||||||
<data name="AllRightsReserved" xml:space="preserve">
|
<data name="AllRightsReserved" xml:space="preserve">
|
||||||
<value>All rights reserved.</value>
|
<value>All rights reserved.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="QRCodesRemainingToday" xml:space="preserve">
|
||||||
|
<value>QR codes remaining today</value>
|
||||||
|
</data>
|
||||||
|
<data name="RateLimitExceeded" xml:space="preserve">
|
||||||
|
<value>Daily limit reached! Login for unlimited access.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AnonymousUserLimit" xml:space="preserve">
|
||||||
|
<value>Anonymous users: 3 QR codes per day</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@ -774,4 +774,13 @@
|
|||||||
<data name="AllRightsReserved" xml:space="preserve">
|
<data name="AllRightsReserved" xml:space="preserve">
|
||||||
<value>Todos los derechos reservados.</value>
|
<value>Todos los derechos reservados.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="QRCodesRemainingToday" xml:space="preserve">
|
||||||
|
<value>Códigos QR restantes hoy</value>
|
||||||
|
</data>
|
||||||
|
<data name="RateLimitExceeded" xml:space="preserve">
|
||||||
|
<value>¡Límite diario alcanzado! Inicia sesión para acceso ilimitado.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AnonymousUserLimit" xml:space="preserve">
|
||||||
|
<value>Usuarios anónimos: 3 códigos QR por día</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@ -774,4 +774,13 @@
|
|||||||
<data name="AllRightsReserved" xml:space="preserve">
|
<data name="AllRightsReserved" xml:space="preserve">
|
||||||
<value>Todos os direitos reservados.</value>
|
<value>Todos os direitos reservados.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="QRCodesRemainingToday" xml:space="preserve">
|
||||||
|
<value>QR codes restantes hoje</value>
|
||||||
|
</data>
|
||||||
|
<data name="RateLimitExceeded" xml:space="preserve">
|
||||||
|
<value>Limite diário atingido! Faça login para acesso ilimitado.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AnonymousUserLimit" xml:space="preserve">
|
||||||
|
<value>Usuários anônimos: 3 QR codes por dia</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@ -16,7 +16,8 @@ namespace QRRapidoApp.Services
|
|||||||
Task<User?> GetUserByStripeCustomerIdAsync(string customerId);
|
Task<User?> GetUserByStripeCustomerIdAsync(string customerId);
|
||||||
Task<bool> UpdateUserAsync(User user);
|
Task<bool> UpdateUserAsync(User user);
|
||||||
Task<int> GetDailyQRCountAsync(string? userId);
|
Task<int> GetDailyQRCountAsync(string? userId);
|
||||||
Task<int> DecrementDailyQRCountAsync(string userId);
|
Task<int> IncrementDailyQRCountAsync(string userId);
|
||||||
|
Task<int> GetRemainingQRCountAsync(string userId);
|
||||||
Task<bool> CanGenerateQRAsync(string? userId, bool isPremium);
|
Task<bool> CanGenerateQRAsync(string? userId, bool isPremium);
|
||||||
Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult);
|
Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult);
|
||||||
Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50);
|
Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50);
|
||||||
|
|||||||
@ -78,7 +78,7 @@ namespace QRRapidoApp.Services
|
|||||||
|
|
||||||
switch (stripeEvent.Type)
|
switch (stripeEvent.Type)
|
||||||
{
|
{
|
||||||
case Events.CheckoutSessionCompleted:
|
case "checkout.session.completed":
|
||||||
var session = stripeEvent.Data.Object as Session;
|
var session = stripeEvent.Data.Object as Session;
|
||||||
if (session?.SubscriptionId != null)
|
if (session?.SubscriptionId != null)
|
||||||
{
|
{
|
||||||
@ -88,12 +88,27 @@ namespace QRRapidoApp.Services
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Events.InvoicePaymentSucceeded:
|
case "invoice.finalized":
|
||||||
var invoice = stripeEvent.Data.Object as Invoice;
|
var invoice = stripeEvent.Data.Object as Invoice;
|
||||||
if (invoice?.SubscriptionId != null)
|
var subscriptionLineItem = invoice.Lines?.Data
|
||||||
|
.FirstOrDefault(line =>
|
||||||
|
!string.IsNullOrEmpty(line.SubscriptionId) ||
|
||||||
|
line.Subscription != null
|
||||||
|
);
|
||||||
|
|
||||||
|
string subscriptionId = null;
|
||||||
|
|
||||||
|
if (subscriptionLineItem != null)
|
||||||
|
{
|
||||||
|
// Tenta obter o ID da assinatura de duas formas diferentes
|
||||||
|
subscriptionId = subscriptionLineItem.SubscriptionId
|
||||||
|
?? subscriptionLineItem.Subscription?.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionId != null)
|
||||||
{
|
{
|
||||||
var subscriptionService = new SubscriptionService();
|
var subscriptionService = new SubscriptionService();
|
||||||
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
var subscription = await subscriptionService.GetAsync(subscriptionId);
|
||||||
var user = await _userService.GetUserByStripeCustomerIdAsync(subscription.CustomerId);
|
var user = await _userService.GetUserByStripeCustomerIdAsync(subscription.CustomerId);
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
@ -102,7 +117,7 @@ namespace QRRapidoApp.Services
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Events.CustomerSubscriptionDeleted:
|
case "customer.subscription.deleted":
|
||||||
var deletedSubscription = stripeEvent.Data.Object as Subscription;
|
var deletedSubscription = stripeEvent.Data.Object as Subscription;
|
||||||
if (deletedSubscription != null)
|
if (deletedSubscription != null)
|
||||||
{
|
{
|
||||||
@ -118,6 +133,8 @@ namespace QRRapidoApp.Services
|
|||||||
|
|
||||||
private async Task ProcessSubscriptionActivation(string userId, Subscription subscription)
|
private async Task ProcessSubscriptionActivation(string userId, Subscription subscription)
|
||||||
{
|
{
|
||||||
|
var service = new SubscriptionItemService();
|
||||||
|
var subItem = service.Get(subscription.Items.Data[0].Id);
|
||||||
if (string.IsNullOrEmpty(userId) || subscription == null)
|
if (string.IsNullOrEmpty(userId) || subscription == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Could not process subscription activation due to missing userId or subscription data.");
|
_logger.LogWarning("Could not process subscription activation due to missing userId or subscription data.");
|
||||||
@ -136,7 +153,8 @@ namespace QRRapidoApp.Services
|
|||||||
await _userService.UpdateUserStripeCustomerIdAsync(user.Id, subscription.CustomerId);
|
await _userService.UpdateUserStripeCustomerIdAsync(user.Id, subscription.CustomerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userService.ActivatePremiumStatus(userId, subscription.Id, subscription.CurrentPeriodEnd);
|
await _userService.ActivatePremiumStatus(userId, subscription.Id, subItem.CurrentPeriodEnd);
|
||||||
|
|
||||||
_logger.LogInformation($"Successfully processed premium activation/renewal for user {userId}.");
|
_logger.LogInformation($"Successfully processed premium activation/renewal for user {userId}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -142,7 +142,7 @@ namespace QRRapidoApp.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> DecrementDailyQRCountAsync(string userId)
|
public async Task<int> IncrementDailyQRCountAsync(string userId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -163,23 +163,47 @@ namespace QRRapidoApp.Services
|
|||||||
user.TotalQRGenerated++;
|
user.TotalQRGenerated++;
|
||||||
await UpdateUserAsync(user);
|
await UpdateUserAsync(user);
|
||||||
|
|
||||||
// Calculate remaining QRs for free users
|
// Premium and logged users have unlimited QR codes
|
||||||
var dailyLimit = user.IsPremium ? int.MaxValue : _config.GetValue<int>("Premium:FreeQRLimit", 50);
|
return int.MaxValue;
|
||||||
return Math.Max(0, dailyLimit - user.DailyQRCount);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Error decrementing daily QR count for user {userId}: {ex.Message}");
|
_logger.LogError(ex, $"Error incrementing daily QR count for user {userId}: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetRemainingQRCountAsync(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await GetUserAsync(userId);
|
||||||
|
if (user == null) return 0;
|
||||||
|
|
||||||
|
// Premium users have unlimited
|
||||||
|
if (user.IsPremium) return int.MaxValue;
|
||||||
|
|
||||||
|
// Logged users (non-premium) have unlimited
|
||||||
|
return int.MaxValue;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting remaining QR count for user {userId}: {ex.Message}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanGenerateQRAsync(string? userId, bool isPremium)
|
public async Task<bool> CanGenerateQRAsync(string? userId, bool isPremium)
|
||||||
{
|
{
|
||||||
|
// Premium users have unlimited QR codes
|
||||||
if (isPremium) return true;
|
if (isPremium) return true;
|
||||||
|
|
||||||
|
// Logged users (non-premium) have unlimited QR codes
|
||||||
|
if (!string.IsNullOrEmpty(userId)) return true;
|
||||||
|
|
||||||
|
// Anonymous users have 3 QR codes per day
|
||||||
var dailyCount = await GetDailyQRCountAsync(userId);
|
var dailyCount = await GetDailyQRCountAsync(userId);
|
||||||
var limit = string.IsNullOrEmpty(userId) ? 10 : _config.GetValue<int>("Premium:FreeQRLimit", 50);
|
var limit = 3;
|
||||||
|
|
||||||
return dailyCount < limit;
|
return dailyCount < limit;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,22 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Hidden input for JavaScript premium status check -->
|
<!-- Hidden input for JavaScript premium status check -->
|
||||||
<input type="hidden" id="user-premium-status" value="@(ViewBag.IsPremium == true ? "true" : "false")" />
|
@{
|
||||||
|
var statusValue = "anonymous";
|
||||||
|
if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
var isPremium = await AdService.HasValidPremiumSubscription(userId);
|
||||||
|
statusValue = isPremium ? "premium" : "logged-in";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<input type="hidden" id="user-premium-status" value="@statusValue" />
|
||||||
|
|
||||||
|
<!-- Hidden localized strings for rate limiting -->
|
||||||
|
<div style="display: none;">
|
||||||
|
<span data-localized="RateLimitExceeded">@Localizer["RateLimitExceeded"]</span>
|
||||||
|
<span data-localized="QRCodesRemainingToday">@Localizer["QRCodesRemainingToday"]</span>
|
||||||
|
<span data-localized="UnlimitedToday">@Localizer["UnlimitedToday"]</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- QR Generator Form -->
|
<!-- QR Generator Form -->
|
||||||
@ -55,18 +70,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
<div class="col-md-4 text-end">
|
||||||
@if (User.Identity.IsAuthenticated)
|
<small class="text-muted">
|
||||||
{
|
<span class="qr-counter">Carregando...</span>
|
||||||
<small class="text-muted">
|
</small>
|
||||||
<span class="qr-counter">@Localizer["UnlimitedToday"]</span>
|
|
||||||
</small>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<small class="text-muted">
|
|
||||||
<span class="qr-counter">@Localizer["QRCodesRemaining"]</span>
|
|
||||||
</small>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
|
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
|
||||||
@{
|
@{
|
||||||
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||||
ViewData["Title"] = "Sucesso";
|
ViewData["Title"] = "Sucesso";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,10 +21,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Stripe": {
|
"Stripe": {
|
||||||
"PublishableKey": "pk_test_xxxxx",
|
"PublishableKey": "pk_test_51Rs42tBeR5IFYUsBooapyDwQTgh6CFuKbya5R3MVDTrdOUKmgiHQYipU0pgOdG5iKogH77RUYIKBJzbCt5BghUOY00xitV5KiN",
|
||||||
"SecretKey": "sk_test_xxxxx",
|
"SecretKey": "sk_test_51Rs42tBeR5IFYUsBtycRlJJcdwgoMbh8MfQIKIGelBPTQFwDcOn2iCCbw5uG6hnqlpgNAUuFgWRAUUMA8qkABKun00EIx4odDF",
|
||||||
"WebhookSecret": "whsec_xxxxx",
|
"WebhookSecret": "whsec_2e828803ceb48e7865458b0cf332b68535fdff8753d26d69b1c88ea55cb0e482",
|
||||||
"PriceId": "price_xxxxx"
|
"PriceId": "prod_SnfQTxwE3i8r5L"
|
||||||
},
|
},
|
||||||
"AdSense": {
|
"AdSense": {
|
||||||
"ClientId": "ca-pub-XXXXXXXXXX",
|
"ClientId": "ca-pub-XXXXXXXXXX",
|
||||||
@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"Premium": {
|
"Premium": {
|
||||||
"FreeQRLimit": 10,
|
"FreeQRLimit": 10,
|
||||||
"PremiumPrice": 19.90,
|
"PremiumPrice": 12.90,
|
||||||
"Features": {
|
"Features": {
|
||||||
"UnlimitedQR": true,
|
"UnlimitedQR": true,
|
||||||
"DynamicQR": true,
|
"DynamicQR": true,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// QR Rapido Speed Generator
|
// QR Rapido Speed Generator
|
||||||
class QRRapidoGenerator {
|
class QRRapidoGenerator {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.startTime = 0;
|
this.startTime = 0;
|
||||||
@ -49,7 +49,9 @@ class QRRapidoGenerator {
|
|||||||
this.checkAdFreeStatus();
|
this.checkAdFreeStatus();
|
||||||
this.updateLanguage();
|
this.updateLanguage();
|
||||||
this.updateStatsCounters();
|
this.updateStatsCounters();
|
||||||
|
this.initializeUserCounter();
|
||||||
this.initializeProgressiveFlow();
|
this.initializeProgressiveFlow();
|
||||||
|
this.initializeRateLimiting();
|
||||||
|
|
||||||
// Validar segurança dos dados após carregamento
|
// Validar segurança dos dados após carregamento
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -345,6 +347,9 @@ class QRRapidoGenerator {
|
|||||||
async generateQRWithTimer(e) {
|
async generateQRWithTimer(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Check rate limit for anonymous users
|
||||||
|
if (!this.checkRateLimit()) return;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!this.validateForm()) return;
|
if (!this.validateForm()) return;
|
||||||
|
|
||||||
@ -708,9 +713,17 @@ class QRRapidoGenerator {
|
|||||||
generationTime: generationTime
|
generationTime: generationTime
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update counter for free users
|
// Increment rate limit counter after successful generation
|
||||||
|
this.incrementRateLimit();
|
||||||
|
|
||||||
|
// Update counter for logged users
|
||||||
if (result.remainingQRs !== undefined) {
|
if (result.remainingQRs !== undefined) {
|
||||||
this.updateRemainingCounter(result.remainingQRs);
|
if (result.remainingQRs === -1 || result.remainingQRs === 2147483647 || result.remainingQRs >= 1000000) {
|
||||||
|
// Logged user - show unlimited
|
||||||
|
this.showUnlimitedCounter();
|
||||||
|
} else {
|
||||||
|
this.updateRemainingCounter(result.remainingQRs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -965,6 +978,27 @@ class QRRapidoGenerator {
|
|||||||
}, 30000); // Update every 30 seconds
|
}, 30000); // Update every 30 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initializeUserCounter() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/QR/GetUserStats');
|
||||||
|
if (response.ok) {
|
||||||
|
const stats = await response.json();
|
||||||
|
console.log('User stats loaded:', stats);
|
||||||
|
console.log('remainingCount:', stats.remainingCount, 'type:', typeof stats.remainingCount);
|
||||||
|
console.log('isUnlimited:', stats.isUnlimited);
|
||||||
|
|
||||||
|
// For logged users, always show unlimited (they all have unlimited QR codes)
|
||||||
|
console.log('Calling showUnlimitedCounter directly for logged user');
|
||||||
|
this.showUnlimitedCounter();
|
||||||
|
} else {
|
||||||
|
console.log('GetUserStats response not ok:', response.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If not authenticated or error, keep the default "Carregando..." text
|
||||||
|
console.debug('User not authenticated or error loading stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trackGenerationEvent(type, time) {
|
trackGenerationEvent(type, time) {
|
||||||
// Google Analytics
|
// Google Analytics
|
||||||
if (typeof gtag !== 'undefined') {
|
if (typeof gtag !== 'undefined') {
|
||||||
@ -1332,8 +1366,15 @@ class QRRapidoGenerator {
|
|||||||
|
|
||||||
updateRemainingCounter(remaining) {
|
updateRemainingCounter(remaining) {
|
||||||
const counterElement = document.querySelector('.qr-counter');
|
const counterElement = document.querySelector('.qr-counter');
|
||||||
if (counterElement) {
|
if (counterElement && remaining !== null && remaining !== undefined) {
|
||||||
counterElement.textContent = `${remaining} QR codes restantes hoje`;
|
// If it's unlimited (any special value indicating unlimited)
|
||||||
|
if (remaining === -1 || remaining >= 2147483647 || remaining > 1000000) {
|
||||||
|
this.showUnlimitedCounter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
|
||||||
|
counterElement.textContent = `${remaining} ${remainingText}`;
|
||||||
|
|
||||||
if (remaining <= 3) {
|
if (remaining <= 3) {
|
||||||
counterElement.className = 'badge bg-warning qr-counter';
|
counterElement.className = 'badge bg-warning qr-counter';
|
||||||
@ -1344,6 +1385,18 @@ class QRRapidoGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showUnlimitedCounter() {
|
||||||
|
console.log('showUnlimitedCounter called');
|
||||||
|
const counterElement = document.querySelector('.qr-counter');
|
||||||
|
if (counterElement) {
|
||||||
|
counterElement.textContent = 'QR Codes ilimitados';
|
||||||
|
counterElement.className = 'badge bg-success qr-counter';
|
||||||
|
console.log('Set counter to: QR Codes ilimitados');
|
||||||
|
} else {
|
||||||
|
console.log('Counter element not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showError(message) {
|
showError(message) {
|
||||||
this.showAlert(message, 'danger');
|
this.showAlert(message, 'danger');
|
||||||
}
|
}
|
||||||
@ -1996,6 +2049,258 @@ class QRRapidoGenerator {
|
|||||||
|
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate Limiting Methods
|
||||||
|
initializeRateLimiting() {
|
||||||
|
// Wait a bit for DOM to be fully ready
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateRateDisplayCounter();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRateLimit() {
|
||||||
|
// Check if user is logged in (unlimited access)
|
||||||
|
const userStatus = document.getElementById('user-premium-status');
|
||||||
|
console.log('🔍 Rate limit check - User status:', userStatus ? userStatus.value : 'not found');
|
||||||
|
|
||||||
|
if (userStatus && userStatus.value === 'logged-in') {
|
||||||
|
console.log('✅ User is logged in - unlimited access');
|
||||||
|
return true; // Unlimited for logged users
|
||||||
|
}
|
||||||
|
|
||||||
|
// For anonymous users, check daily limit
|
||||||
|
const today = new Date().toDateString();
|
||||||
|
const cookieName = 'qr_daily_count';
|
||||||
|
const rateLimitData = this.getCookie(cookieName);
|
||||||
|
|
||||||
|
console.log('📅 Today:', today);
|
||||||
|
console.log('🍪 Cookie data:', rateLimitData);
|
||||||
|
|
||||||
|
let currentData = { date: today, count: 0 };
|
||||||
|
|
||||||
|
if (rateLimitData) {
|
||||||
|
try {
|
||||||
|
currentData = JSON.parse(rateLimitData);
|
||||||
|
console.log('📊 Parsed data:', currentData);
|
||||||
|
|
||||||
|
// Reset count if it's a new day
|
||||||
|
if (currentData.date !== today) {
|
||||||
|
console.log('🔄 New day detected, resetting count');
|
||||||
|
currentData = { date: today, count: 0 };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('❌ Error parsing cookie:', e);
|
||||||
|
currentData = { date: today, count: 0 };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('🆕 No cookie found, starting fresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📈 Current count:', currentData.count);
|
||||||
|
|
||||||
|
// Check if limit exceeded (don't increment here)
|
||||||
|
if (currentData.count >= 3) {
|
||||||
|
console.log('🚫 Rate limit exceeded');
|
||||||
|
this.showRateLimitError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Rate limit check passed');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementRateLimit() {
|
||||||
|
// Only increment after successful QR generation
|
||||||
|
const userStatus = document.getElementById('user-premium-status');
|
||||||
|
if (userStatus && userStatus.value === 'logged-in') {
|
||||||
|
return; // No limits for logged users
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date().toDateString();
|
||||||
|
const cookieName = 'qr_daily_count';
|
||||||
|
const rateLimitData = this.getCookie(cookieName);
|
||||||
|
|
||||||
|
let currentData = { date: today, count: 0 };
|
||||||
|
|
||||||
|
if (rateLimitData) {
|
||||||
|
try {
|
||||||
|
currentData = JSON.parse(rateLimitData);
|
||||||
|
if (currentData.date !== today) {
|
||||||
|
currentData = { date: today, count: 0 };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
currentData = { date: today, count: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment count and save
|
||||||
|
currentData.count++;
|
||||||
|
this.setCookie(cookieName, JSON.stringify(currentData), 1);
|
||||||
|
|
||||||
|
// Update display counter
|
||||||
|
this.updateRateDisplayCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
showRateLimitError() {
|
||||||
|
const message = this.getLocalizedString('RateLimitExceeded') || 'Daily limit reached! Login for unlimited access.';
|
||||||
|
this.showError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRateDisplayCounter() {
|
||||||
|
const counterElement = document.querySelector('.qr-counter');
|
||||||
|
if (!counterElement) return;
|
||||||
|
|
||||||
|
// Check user status
|
||||||
|
const userStatus = document.getElementById('user-premium-status');
|
||||||
|
|
||||||
|
if (userStatus && userStatus.value === 'premium') {
|
||||||
|
// Premium users have unlimited QRs
|
||||||
|
const unlimitedText = this.getLocalizedString('UnlimitedToday');
|
||||||
|
counterElement.textContent = unlimitedText;
|
||||||
|
counterElement.className = 'badge bg-success qr-counter';
|
||||||
|
return;
|
||||||
|
} else if (userStatus && userStatus.value === 'logged-in') {
|
||||||
|
// Free logged users - we need to get their actual remaining count
|
||||||
|
this.updateLoggedUserCounter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For anonymous users, show remaining count
|
||||||
|
const today = new Date().toDateString();
|
||||||
|
const cookieName = 'qr_daily_count';
|
||||||
|
const rateLimitData = this.getCookie(cookieName);
|
||||||
|
|
||||||
|
let remaining = 3;
|
||||||
|
|
||||||
|
if (rateLimitData) {
|
||||||
|
try {
|
||||||
|
const currentData = JSON.parse(rateLimitData);
|
||||||
|
if (currentData.date === today) {
|
||||||
|
remaining = Math.max(0, 3 - currentData.count);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
remaining = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
|
||||||
|
counterElement.textContent = `${remaining} ${remainingText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLoggedUserCounter() {
|
||||||
|
const counterElement = document.querySelector('.qr-counter');
|
||||||
|
if (!counterElement) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch user's remaining QR count from the server
|
||||||
|
const response = await fetch('/api/QR/GetUserStats', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.isPremium) {
|
||||||
|
// Premium user - show unlimited
|
||||||
|
const unlimitedText = this.getLocalizedString('UnlimitedToday');
|
||||||
|
counterElement.textContent = unlimitedText;
|
||||||
|
counterElement.className = 'badge bg-success qr-counter';
|
||||||
|
} else {
|
||||||
|
// Free user - show remaining count
|
||||||
|
const remaining = data.remainingCount || 0;
|
||||||
|
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
|
||||||
|
if (remaining !== -1) {
|
||||||
|
counterElement.textContent = `${remaining} ${remainingText}`;
|
||||||
|
|
||||||
|
counterElement.className = 'badge bg-primary qr-counter';
|
||||||
|
if (remaining <= 3) {
|
||||||
|
counterElement.className = 'badge bg-warning qr-counter';
|
||||||
|
}
|
||||||
|
if (remaining === 0) {
|
||||||
|
counterElement.className = 'badge bg-danger qr-counter';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const unlimitedText = this.getLocalizedString('UnlimitedToday');
|
||||||
|
counterElement.textContent = unlimitedText;
|
||||||
|
counterElement.className = 'badge bg-success qr-counter';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to showing 50 remaining if API fails
|
||||||
|
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
|
||||||
|
counterElement.textContent = `50 ${remainingText}`;
|
||||||
|
counterElement.className = 'badge bg-primary qr-counter';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch user stats:', error);
|
||||||
|
// Fallback to showing 50 remaining if API fails
|
||||||
|
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
|
||||||
|
counterElement.textContent = `50 ${remainingText}`;
|
||||||
|
counterElement.className = 'badge bg-primary qr-counter';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalizedString(key) {
|
||||||
|
// Try to get from server-side localization first
|
||||||
|
const element = document.querySelector(`[data-localized="${key}"]`);
|
||||||
|
if (element) {
|
||||||
|
const text = element.textContent.trim() || element.value;
|
||||||
|
if (text) return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to client-side strings
|
||||||
|
if (this.languageStrings[this.currentLang] && this.languageStrings[this.currentLang][key]) {
|
||||||
|
return this.languageStrings[this.currentLang][key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallbacks based on key
|
||||||
|
const defaults = {
|
||||||
|
'UnlimitedToday': 'Ilimitado hoje',
|
||||||
|
'QRCodesRemainingToday': 'QR codes restantes hoje',
|
||||||
|
'RateLimitExceeded': 'Limite diário atingido! Faça login para acesso ilimitado.'
|
||||||
|
};
|
||||||
|
|
||||||
|
return defaults[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie(name, value, days) {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Strict`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCookie(name) {
|
||||||
|
const nameEQ = name + "=";
|
||||||
|
const ca = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug/reset function - call from console if needed
|
||||||
|
resetRateLimit() {
|
||||||
|
// Multiple ways to clear the cookie
|
||||||
|
document.cookie = 'qr_daily_count=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||||
|
document.cookie = 'qr_daily_count=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=' + window.location.hostname + ';';
|
||||||
|
document.cookie = 'qr_daily_count=; max-age=0; path=/;';
|
||||||
|
|
||||||
|
// Also clear from storage if exists
|
||||||
|
if (typeof Storage !== "undefined") {
|
||||||
|
localStorage.removeItem('qr_daily_count');
|
||||||
|
sessionStorage.removeItem('qr_daily_count');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRateDisplayCounter();
|
||||||
|
console.log('🧹 Rate limit completely reset!');
|
||||||
|
console.log('🍪 All cookies:', document.cookie);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize when DOM loads
|
// Initialize when DOM loads
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user