fix: ajustar para configuração de plano ficam só no mongondb.
This commit is contained in:
parent
59e04fedc7
commit
2edb4e1196
@ -29,8 +29,10 @@
|
|||||||
"Bash(nc:*)",
|
"Bash(nc:*)",
|
||||||
"Bash(ssh:*)",
|
"Bash(ssh:*)",
|
||||||
"Read(//mnt/c/vscode/**)",
|
"Read(//mnt/c/vscode/**)",
|
||||||
"Read(//mnt/c/**)"
|
"Read(//mnt/c/**)",
|
||||||
|
"Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)",
|
||||||
|
"Bash(netstat -tln)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
142
Controllers/AdminController.cs
Normal file
142
Controllers/AdminController.cs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QRRapidoApp.Data;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Admin controller - ONLY accessible from localhost for security
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AdminController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly MongoDbContext _context;
|
||||||
|
private readonly ILogger<AdminController> _logger;
|
||||||
|
|
||||||
|
public AdminController(MongoDbContext context, ILogger<AdminController> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seed/Update MongoDB Plans collection
|
||||||
|
/// Only accessible from localhost (127.0.0.1 or ::1)
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("SeedPlans")]
|
||||||
|
public async Task<IActionResult> SeedPlans([FromBody] List<Plan> plans)
|
||||||
|
{
|
||||||
|
// SECURITY: Only allow from localhost
|
||||||
|
var remoteIp = HttpContext.Connection.RemoteIpAddress;
|
||||||
|
var isLocalhost = remoteIp != null &&
|
||||||
|
(remoteIp.ToString() == "127.0.0.1" ||
|
||||||
|
remoteIp.ToString() == "::1" ||
|
||||||
|
remoteIp.ToString() == "localhost");
|
||||||
|
|
||||||
|
if (!isLocalhost)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}");
|
||||||
|
return Forbid("This endpoint is only accessible from localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"SeedPlans called from localhost - Upserting {plans.Count} plans");
|
||||||
|
|
||||||
|
foreach (var plan in plans)
|
||||||
|
{
|
||||||
|
// Upsert based on interval (month/year)
|
||||||
|
var filter = Builders<Plan>.Filter.Eq(p => p.Interval, plan.Interval);
|
||||||
|
var options = new ReplaceOptions { IsUpsert = true };
|
||||||
|
|
||||||
|
await _context.Plans.ReplaceOneAsync(filter, plan, options);
|
||||||
|
_logger.LogInformation($"Upserted plan: {plan.Interval}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new {
|
||||||
|
success = true,
|
||||||
|
message = $"{plans.Count} plans seeded successfully",
|
||||||
|
plans = plans.Select(p => new {
|
||||||
|
interval = p.Interval,
|
||||||
|
priceIds = p.PricesByCountry.ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => kvp.Value.StripePriceId
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error seeding plans");
|
||||||
|
return StatusCode(500, new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all plans from MongoDB
|
||||||
|
/// Only accessible from localhost
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("Plans")]
|
||||||
|
public async Task<IActionResult> GetPlans()
|
||||||
|
{
|
||||||
|
// SECURITY: Only allow from localhost
|
||||||
|
var remoteIp = HttpContext.Connection.RemoteIpAddress;
|
||||||
|
var isLocalhost = remoteIp != null &&
|
||||||
|
(remoteIp.ToString() == "127.0.0.1" ||
|
||||||
|
remoteIp.ToString() == "::1" ||
|
||||||
|
remoteIp.ToString() == "localhost");
|
||||||
|
|
||||||
|
if (!isLocalhost)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}");
|
||||||
|
return Forbid("This endpoint is only accessible from localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var plans = await _context.Plans.Find(_ => true).ToListAsync();
|
||||||
|
return Ok(new { success = true, count = plans.Count, plans });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving plans");
|
||||||
|
return StatusCode(500, new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete all plans from MongoDB
|
||||||
|
/// Only accessible from localhost
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("Plans")]
|
||||||
|
public async Task<IActionResult> DeleteAllPlans()
|
||||||
|
{
|
||||||
|
// SECURITY: Only allow from localhost
|
||||||
|
var remoteIp = HttpContext.Connection.RemoteIpAddress;
|
||||||
|
var isLocalhost = remoteIp != null &&
|
||||||
|
(remoteIp.ToString() == "127.0.0.1" ||
|
||||||
|
remoteIp.ToString() == "::1" ||
|
||||||
|
remoteIp.ToString() == "localhost");
|
||||||
|
|
||||||
|
if (!isLocalhost)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}");
|
||||||
|
return Forbid("This endpoint is only accessible from localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _context.Plans.DeleteManyAsync(_ => true);
|
||||||
|
_logger.LogInformation($"Deleted {result.DeletedCount} plans");
|
||||||
|
return Ok(new { success = true, deletedCount = result.DeletedCount });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting plans");
|
||||||
|
return StatusCode(500, new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,172 +1,172 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using QRRapidoApp.Services;
|
using QRRapidoApp.Services;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using QRRapidoApp.Models.ViewModels;
|
using QRRapidoApp.Models.ViewModels;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace QRRapidoApp.Controllers
|
namespace QRRapidoApp.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class PagamentoController : Controller
|
public class PagamentoController : Controller
|
||||||
{
|
{
|
||||||
private readonly IPlanService _planService;
|
private readonly IPlanService _planService;
|
||||||
private readonly AdDisplayService _adDisplayService;
|
private readonly AdDisplayService _adDisplayService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly StripeService _stripeService;
|
private readonly StripeService _stripeService;
|
||||||
private readonly ILogger<PagamentoController> _logger;
|
private readonly ILogger<PagamentoController> _logger;
|
||||||
private readonly List<string> languages = new List<string> { "pt-BR", "es-PY", "es" };
|
private readonly List<string> languages = new List<string> { "pt-BR", "es-PY", "es" };
|
||||||
|
|
||||||
public PagamentoController(IPlanService planService, IUserService userService, StripeService stripeService, ILogger<PagamentoController> logger, AdDisplayService adDisplayService)
|
public PagamentoController(IPlanService planService, IUserService userService, StripeService stripeService, ILogger<PagamentoController> logger, AdDisplayService adDisplayService)
|
||||||
{
|
{
|
||||||
_planService = planService;
|
_planService = planService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_stripeService = stripeService;
|
_stripeService = stripeService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_adDisplayService = adDisplayService;
|
_adDisplayService = adDisplayService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> SelecaoPlano()
|
public async Task<IActionResult> SelecaoPlano()
|
||||||
{
|
{
|
||||||
var plans = await _planService.GetActivePlansAsync();
|
var plans = await _planService.GetActivePlansAsync();
|
||||||
var countryCode = GetUserCountryCodeComplete(); // Implement this method based on your needs
|
var countryCode = GetUserCountryCodeComplete(); // Implement this method based on your needs
|
||||||
_adDisplayService.SetViewBagAds(ViewBag);
|
_adDisplayService.SetViewBagAds(ViewBag);
|
||||||
|
|
||||||
var model = new SelecaoPlanoViewModel
|
var model = new SelecaoPlanoViewModel
|
||||||
{
|
{
|
||||||
Plans = plans,
|
Plans = plans,
|
||||||
CountryCode = countryCode
|
CountryCode = countryCode
|
||||||
};
|
};
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateCheckout(string planId, string lang)
|
public async Task<IActionResult> CreateCheckout(string planId, string lang)
|
||||||
{
|
{
|
||||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
if (string.IsNullOrEmpty(userId))
|
if (string.IsNullOrEmpty(userId))
|
||||||
{
|
{
|
||||||
return Json(new { success = false, error = "User not authenticated" });
|
return Json(new { success = false, error = "User not authenticated" });
|
||||||
}
|
}
|
||||||
|
|
||||||
var plan = await _planService.GetPlanByIdAsync(planId);
|
var plan = await _planService.GetPlanByIdAsync(planId);
|
||||||
if (plan == null)
|
if (plan == null)
|
||||||
{
|
{
|
||||||
return Json(new { success = false, error = "Plan not found" });
|
return Json(new { success = false, error = "Plan not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
var countryCode = GetUserCountryCode();
|
var countryCode = GetUserCountryCode();
|
||||||
if (countryCode != lang && languages.Contains(lang))
|
if (countryCode != lang && languages.Contains(lang))
|
||||||
{
|
{
|
||||||
countryCode = lang;
|
countryCode = lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
var priceId = plan.PricesByCountry.ContainsKey(countryCode)
|
var priceId = plan.PricesByCountry.ContainsKey(countryCode)
|
||||||
? plan.PricesByCountry[countryCode].StripePriceId
|
? plan.PricesByCountry[countryCode].StripePriceId
|
||||||
: plan.StripePriceId;
|
: plan.StripePriceId;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId, lang);
|
var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId, lang);
|
||||||
return Json(new { success = true, url = checkoutUrl });
|
return Json(new { success = true, url = checkoutUrl });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Error creating checkout session for user {userId} and plan {planId}");
|
_logger.LogError(ex, $"Error creating checkout session for user {userId} and plan {planId}");
|
||||||
return Json(new { success = false, error = ex.Message });
|
return Json(new { success = false, error = ex.Message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Sucesso()
|
public IActionResult Sucesso()
|
||||||
{
|
{
|
||||||
_adDisplayService.SetViewBagAds(ViewBag);
|
_adDisplayService.SetViewBagAds(ViewBag);
|
||||||
ViewBag.SuccessMessage = "Pagamento concluído com sucesso! Bem-vindo ao Premium.";
|
ViewBag.SuccessMessage = "Pagamento concluído com sucesso! Bem-vindo ao Premium.";
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Cancelar()
|
public async Task<IActionResult> Cancelar()
|
||||||
{
|
{
|
||||||
_adDisplayService.SetViewBagAds(ViewBag);
|
_adDisplayService.SetViewBagAds(ViewBag);
|
||||||
ViewBag.CancelMessage = "O pagamento foi cancelado. Você pode tentar novamente a qualquer momento.";
|
ViewBag.CancelMessage = "O pagamento foi cancelado. Você pode tentar novamente a qualquer momento.";
|
||||||
|
|
||||||
var plans = await _planService.GetActivePlansAsync();
|
var plans = await _planService.GetActivePlansAsync();
|
||||||
var countryCode = GetUserCountryCode(); // Implement this method based on your needs
|
var countryCode = GetUserCountryCode(); // Implement this method based on your needs
|
||||||
_adDisplayService.SetViewBagAds(ViewBag);
|
_adDisplayService.SetViewBagAds(ViewBag);
|
||||||
|
|
||||||
var model = new SelecaoPlanoViewModel
|
var model = new SelecaoPlanoViewModel
|
||||||
{
|
{
|
||||||
Plans = plans,
|
Plans = plans,
|
||||||
CountryCode = countryCode
|
CountryCode = countryCode
|
||||||
};
|
};
|
||||||
|
|
||||||
return View("SelecaoPlano", model);
|
return View("SelecaoPlano", model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> StripeWebhook()
|
public async Task<IActionResult> StripeWebhook()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(HttpContext.Request.Body);
|
using var reader = new StreamReader(HttpContext.Request.Body);
|
||||||
var json = await reader.ReadToEndAsync();
|
var json = await reader.ReadToEndAsync();
|
||||||
var signature = Request.Headers["Stripe-Signature"].FirstOrDefault();
|
var signature = Request.Headers["Stripe-Signature"].FirstOrDefault();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(signature))
|
if (string.IsNullOrEmpty(signature))
|
||||||
{
|
{
|
||||||
return BadRequest("Missing Stripe signature");
|
return BadRequest("Missing Stripe signature");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _stripeService.HandleWebhookAsync(json, signature);
|
await _stripeService.HandleWebhookAsync(json, signature);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error processing Stripe webhook");
|
_logger.LogError(ex, "Error processing Stripe webhook");
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetUserCountryCode()
|
private string GetUserCountryCode()
|
||||||
{
|
{
|
||||||
// Check current culture from URL first
|
// Check current culture from URL first
|
||||||
var culture = HttpContext.Request.RouteValues["culture"]?.ToString() ??
|
var culture = HttpContext.Request.RouteValues["culture"]?.ToString() ??
|
||||||
HttpContext.Features.Get<Microsoft.AspNetCore.Localization.IRequestCultureFeature>()?.RequestCulture?.Culture?.Name;
|
HttpContext.Features.Get<Microsoft.AspNetCore.Localization.IRequestCultureFeature>()?.RequestCulture?.Culture?.Name;
|
||||||
|
|
||||||
var countryMap = new Dictionary<string, string>
|
var countryMap = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "pt-BR", "BR" },
|
{ "pt-BR", "BR" },
|
||||||
{ "es-PY", "PY" },
|
{ "es-PY", "PY" },
|
||||||
{ "es", "PY" }
|
{ "es", "PY" }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(culture) && countryMap.ContainsKey(culture))
|
if (!string.IsNullOrEmpty(culture) && countryMap.ContainsKey(culture))
|
||||||
{
|
{
|
||||||
return countryMap[culture];
|
return countryMap[culture];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Cloudflare header or default
|
// Fallback to Cloudflare header or default
|
||||||
return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR";
|
return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR";
|
||||||
}
|
}
|
||||||
private string GetUserCountryCodeComplete()
|
private string GetUserCountryCodeComplete()
|
||||||
{
|
{
|
||||||
// Check current culture from URL first
|
// Check current culture from URL first
|
||||||
var culture = HttpContext.Request.RouteValues["culture"]?.ToString() ??
|
var culture = HttpContext.Request.RouteValues["culture"]?.ToString() ??
|
||||||
HttpContext.Features.Get<Microsoft.AspNetCore.Localization.IRequestCultureFeature>()?.RequestCulture?.Culture?.Name;
|
HttpContext.Features.Get<Microsoft.AspNetCore.Localization.IRequestCultureFeature>()?.RequestCulture?.Culture?.Name;
|
||||||
|
|
||||||
if (languages.Contains(culture))
|
if (languages.Contains(culture))
|
||||||
{
|
{
|
||||||
return culture;
|
return culture;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Cloudflare header or default
|
// Fallback to Cloudflare header or default
|
||||||
return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR";
|
return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"applicationUrl": "https://localhost:52428;http://localhost:52429"
|
"applicationUrl": "https://0.0.0.0:52428;http://0.0.0.0:52429"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
272
Scripts/README-ADMIN-API.md
Normal file
272
Scripts/README-ADMIN-API.md
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
# 🔧 Admin API - Gerenciamento de Planos
|
||||||
|
|
||||||
|
Este diretório contém ferramentas para gerenciar os planos do Stripe no MongoDB via API local.
|
||||||
|
|
||||||
|
## 🔒 Segurança
|
||||||
|
|
||||||
|
**IMPORTANTE**: O endpoint `/api/Admin/*` **só funciona quando acessado de localhost** (127.0.0.1). Qualquer acesso externo retornará `403 Forbidden`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Arquivos
|
||||||
|
|
||||||
|
- **`plans.json`** - Configuração dos planos com Price IDs do Stripe
|
||||||
|
- **`update-plans.sh`** - Script bash para atualizar planos via curl
|
||||||
|
- **`seed-mongodb-plans.js`** - Script MongoDB direto (método alternativo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Método 1: API Endpoint (Recomendado)
|
||||||
|
|
||||||
|
### Vantagens
|
||||||
|
- ✅ Não precisa de credenciais do MongoDB
|
||||||
|
- ✅ Validação automática
|
||||||
|
- ✅ Logs no sistema
|
||||||
|
- ✅ Funciona com app rodando
|
||||||
|
|
||||||
|
### Como Usar
|
||||||
|
|
||||||
|
#### 1️⃣ Edite o `plans.json` com seus Price IDs
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pricesByCountry": {
|
||||||
|
"BR": {
|
||||||
|
"stripePriceId": "price_SEU_ID_AQUI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2️⃣ Inicie a aplicação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
# ou
|
||||||
|
dotnet watch run
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3️⃣ Execute o script de atualização
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/Mac/WSL
|
||||||
|
cd Scripts
|
||||||
|
./update-plans.sh
|
||||||
|
|
||||||
|
# Especificar porta diferente
|
||||||
|
./update-plans.sh 5000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4️⃣ Ou use curl diretamente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Atualizar planos
|
||||||
|
curl -k -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @Scripts/plans.json \
|
||||||
|
https://localhost:52428/api/Admin/SeedPlans
|
||||||
|
|
||||||
|
# Listar planos
|
||||||
|
curl -k https://localhost:52428/api/Admin/Plans
|
||||||
|
|
||||||
|
# Deletar todos os planos
|
||||||
|
curl -k -X DELETE https://localhost:52428/api/Admin/Plans
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Método 2: Script MongoDB Direto
|
||||||
|
|
||||||
|
### Vantagens
|
||||||
|
- ✅ Funciona sem a app rodando
|
||||||
|
- ✅ Acesso direto ao MongoDB
|
||||||
|
|
||||||
|
### Desvantagens
|
||||||
|
- ❌ Precisa de credenciais do MongoDB
|
||||||
|
- ❌ Sem validação automática
|
||||||
|
|
||||||
|
### Como Usar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Localhost
|
||||||
|
mongosh "mongodb://localhost:27017/QrRapido" < Scripts/seed-mongodb-plans.js
|
||||||
|
|
||||||
|
# Produção
|
||||||
|
mongosh "mongodb://user:pass@host:27017/QrRapido?replicaSet=rs0&authSource=admin" < Scripts/seed-mongodb-plans.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Endpoints Disponíveis
|
||||||
|
|
||||||
|
### POST `/api/Admin/SeedPlans`
|
||||||
|
Cria ou atualiza planos no MongoDB
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"interval": "month",
|
||||||
|
"stripePriceId": "price_...",
|
||||||
|
"pricesByCountry": { ... }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "2 plans seeded successfully",
|
||||||
|
"plans": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET `/api/Admin/Plans`
|
||||||
|
Lista todos os planos do MongoDB
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"count": 2,
|
||||||
|
"plans": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DELETE `/api/Admin/Plans`
|
||||||
|
Remove todos os planos do MongoDB
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"deletedCount": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Proteção de Segurança
|
||||||
|
|
||||||
|
O código verifica o IP de origem:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var remoteIp = HttpContext.Connection.RemoteIpAddress;
|
||||||
|
var isLocalhost = remoteIp != null &&
|
||||||
|
(remoteIp.ToString() == "127.0.0.1" ||
|
||||||
|
remoteIp.ToString() == "::1" ||
|
||||||
|
remoteIp.ToString() == "localhost");
|
||||||
|
|
||||||
|
if (!isLocalhost) {
|
||||||
|
return Forbid("This endpoint is only accessible from localhost");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tentativas de acesso externo são logadas:**
|
||||||
|
```
|
||||||
|
[Warning] Unauthorized admin access attempt from 192.168.1.100
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testando
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Listar planos atuais
|
||||||
|
curl -k https://localhost:52428/api/Admin/Plans | jq '.'
|
||||||
|
|
||||||
|
# 2. Deletar planos
|
||||||
|
curl -k -X DELETE https://localhost:52428/api/Admin/Plans
|
||||||
|
|
||||||
|
# 3. Criar novos planos
|
||||||
|
curl -k -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @Scripts/plans.json \
|
||||||
|
https://localhost:52428/api/Admin/SeedPlans | jq '.'
|
||||||
|
|
||||||
|
# 4. Verificar se foram criados
|
||||||
|
curl -k https://localhost:52428/api/Admin/Plans | jq '.plans[] | {interval, priceIds: .pricesByCountry}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Troubleshooting
|
||||||
|
|
||||||
|
### Erro: "Connection refused"
|
||||||
|
- ✅ Certifique-se que a app está rodando: `dotnet run`
|
||||||
|
|
||||||
|
### Erro: "Forbidden" mesmo em localhost
|
||||||
|
- ✅ Verifique se está usando `https://localhost` (não `http://`)
|
||||||
|
- ✅ Verifique se está usando `localhost` ou `127.0.0.1` (não o IP da máquina)
|
||||||
|
|
||||||
|
### Erro: "SSL certificate problem"
|
||||||
|
- ✅ Use o flag `-k` no curl para aceitar certificados auto-assinados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Estrutura do plans.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"pt-BR": "Nome em português",
|
||||||
|
"es-PY": "Nombre en español",
|
||||||
|
"en": "Name in english"
|
||||||
|
},
|
||||||
|
"description": { ... },
|
||||||
|
"features": { ... },
|
||||||
|
"interval": "month" ou "year",
|
||||||
|
"stripePriceId": "price_default",
|
||||||
|
"pricesByCountry": {
|
||||||
|
"BR": {
|
||||||
|
"amount": 9.90,
|
||||||
|
"currency": "BRL",
|
||||||
|
"stripePriceId": "price_BR_ID"
|
||||||
|
},
|
||||||
|
"PY": {
|
||||||
|
"amount": 35000,
|
||||||
|
"currency": "PYG",
|
||||||
|
"stripePriceId": "price_PY_ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isActive": true,
|
||||||
|
"displayOrder": 1,
|
||||||
|
"badge": { ... } // Opcional
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Fluxo Recomendado
|
||||||
|
|
||||||
|
1. **Desenvolvimento (localhost)**:
|
||||||
|
```bash
|
||||||
|
# Editar plans.json com Price IDs de teste
|
||||||
|
./update-plans.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Staging**:
|
||||||
|
```bash
|
||||||
|
# SSH no servidor staging
|
||||||
|
ssh user@staging-server
|
||||||
|
cd /app/qrrapido
|
||||||
|
./Scripts/update-plans.sh 5000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Produção**:
|
||||||
|
```bash
|
||||||
|
# Opção 1: Via API (se tiver acesso SSH)
|
||||||
|
ssh user@prod-server
|
||||||
|
cd /app/qrrapido
|
||||||
|
./Scripts/update-plans.sh 5001
|
||||||
|
|
||||||
|
# Opção 2: Via MongoDB direto
|
||||||
|
mongosh "connection_string" < Scripts/seed-mongodb-plans.js
|
||||||
|
```
|
||||||
134
Scripts/plans.json
Normal file
134
Scripts/plans.json
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"pt-BR": "Premium Mensal",
|
||||||
|
"es-PY": "Premium Mensual",
|
||||||
|
"en": "Premium Monthly"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"pt-BR": "Acesso ilimitado a todos os recursos premium - Cobrança mensal",
|
||||||
|
"es-PY": "Acceso ilimitado a todas las funciones premium - Facturación mensual",
|
||||||
|
"en": "Unlimited access to all premium features - Monthly billing"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"pt-BR": [
|
||||||
|
"QR Codes ilimitados",
|
||||||
|
"Sem anúncios",
|
||||||
|
"Customização avançada",
|
||||||
|
"Suporte para logos",
|
||||||
|
"Histórico e downloads",
|
||||||
|
"Suporte prioritário",
|
||||||
|
"Cancele a qualquer momento"
|
||||||
|
],
|
||||||
|
"es-PY": [
|
||||||
|
"Códigos QR ilimitados",
|
||||||
|
"Sin anuncios",
|
||||||
|
"Personalización avanzada",
|
||||||
|
"Soporte para logos",
|
||||||
|
"Historial y descargas",
|
||||||
|
"Soporte prioritario",
|
||||||
|
"Cancela cuando quieras"
|
||||||
|
],
|
||||||
|
"en": [
|
||||||
|
"Unlimited QR Codes",
|
||||||
|
"No ads",
|
||||||
|
"Advanced customization",
|
||||||
|
"Logo support",
|
||||||
|
"History and downloads",
|
||||||
|
"Priority support",
|
||||||
|
"Cancel anytime"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"interval": "month",
|
||||||
|
"stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6",
|
||||||
|
"pricesByCountry": {
|
||||||
|
"BR": {
|
||||||
|
"amount": 9.90,
|
||||||
|
"currency": "BRL",
|
||||||
|
"stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6"
|
||||||
|
},
|
||||||
|
"PY": {
|
||||||
|
"amount": 35000,
|
||||||
|
"currency": "PYG",
|
||||||
|
"stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi"
|
||||||
|
},
|
||||||
|
"US": {
|
||||||
|
"amount": 1.99,
|
||||||
|
"currency": "USD",
|
||||||
|
"stripePriceId": "price_XXXXX_monthly_us"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isActive": true,
|
||||||
|
"displayOrder": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"pt-BR": "Premium Anual",
|
||||||
|
"es-PY": "Premium Anual",
|
||||||
|
"en": "Premium Yearly"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"pt-BR": "Acesso ilimitado a todos os recursos premium - Economia de 20% no plano anual!",
|
||||||
|
"es-PY": "Acceso ilimitado a todas las funciones premium - ¡Ahorra 20% con el plan anual!",
|
||||||
|
"en": "Unlimited access to all premium features - Save 20% with yearly billing!"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"pt-BR": [
|
||||||
|
"QR Codes ilimitados",
|
||||||
|
"Sem anúncios",
|
||||||
|
"Customização avançada",
|
||||||
|
"Suporte para logos",
|
||||||
|
"Histórico e downloads",
|
||||||
|
"Suporte prioritário",
|
||||||
|
"💰 Economia de 20%",
|
||||||
|
"Cobrança anual única"
|
||||||
|
],
|
||||||
|
"es-PY": [
|
||||||
|
"Códigos QR ilimitados",
|
||||||
|
"Sin anuncios",
|
||||||
|
"Personalización avanzada",
|
||||||
|
"Soporte para logos",
|
||||||
|
"Historial y descargas",
|
||||||
|
"Soporte prioritario",
|
||||||
|
"💰 Ahorro del 20%",
|
||||||
|
"Facturación anual única"
|
||||||
|
],
|
||||||
|
"en": [
|
||||||
|
"Unlimited QR Codes",
|
||||||
|
"No ads",
|
||||||
|
"Advanced customization",
|
||||||
|
"Logo support",
|
||||||
|
"History and downloads",
|
||||||
|
"Priority support",
|
||||||
|
"💰 Save 20%",
|
||||||
|
"Billed annually"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"interval": "year",
|
||||||
|
"stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4",
|
||||||
|
"pricesByCountry": {
|
||||||
|
"BR": {
|
||||||
|
"amount": 95.04,
|
||||||
|
"currency": "BRL",
|
||||||
|
"stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4"
|
||||||
|
},
|
||||||
|
"PY": {
|
||||||
|
"amount": 336000,
|
||||||
|
"currency": "PYG",
|
||||||
|
"stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi"
|
||||||
|
},
|
||||||
|
"US": {
|
||||||
|
"amount": 19.10,
|
||||||
|
"currency": "USD",
|
||||||
|
"stripePriceId": "price_XXXXX_yearly_us"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isActive": true,
|
||||||
|
"displayOrder": 2,
|
||||||
|
"badge": {
|
||||||
|
"pt-BR": "MELHOR VALOR",
|
||||||
|
"es-PY": "MEJOR VALOR",
|
||||||
|
"en": "BEST VALUE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -51,22 +51,22 @@ db.Plans.insertOne({
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
interval: "month",
|
interval: "month",
|
||||||
stripePriceId: "price_XXXXX_monthly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Monthly:US
|
stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6", // Default price (BR)
|
||||||
pricesByCountry: {
|
pricesByCountry: {
|
||||||
"BR": {
|
"BR": {
|
||||||
amount: 9.90,
|
amount: 9.90,
|
||||||
currency: "BRL",
|
currency: "BRL",
|
||||||
stripePriceId: "price_XXXXX_monthly_br" // UPDATE from appsettings.json > Stripe:Plans:Monthly:BR
|
stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6"
|
||||||
},
|
},
|
||||||
"PY": {
|
"PY": {
|
||||||
amount: 35000,
|
amount: 35000,
|
||||||
currency: "PYG",
|
currency: "PYG",
|
||||||
stripePriceId: "price_XXXXX_monthly_py" // UPDATE from appsettings.json > Stripe:Plans:Monthly:PY
|
stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi"
|
||||||
},
|
},
|
||||||
"US": {
|
"US": {
|
||||||
amount: 1.99,
|
amount: 1.99,
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
stripePriceId: "price_XXXXX_monthly_us" // UPDATE from appsettings.json > Stripe:Plans:Monthly:US
|
stripePriceId: "price_XXXXX_monthly_us" // TODO: Update with real Stripe Price ID
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@ -120,22 +120,22 @@ db.Plans.insertOne({
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
interval: "year",
|
interval: "year",
|
||||||
stripePriceId: "price_XXXXX_yearly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Yearly:US
|
stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4", // Default price (BR)
|
||||||
pricesByCountry: {
|
pricesByCountry: {
|
||||||
"BR": {
|
"BR": {
|
||||||
amount: 95.04, // 9.90 * 12 * 0.80 = Economia de 20%
|
amount: 95.04, // 9.90 * 12 * 0.80 = Economia de 20%
|
||||||
currency: "BRL",
|
currency: "BRL",
|
||||||
stripePriceId: "price_XXXXX_yearly_br" // UPDATE from appsettings.json > Stripe:Plans:Yearly:BR
|
stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4"
|
||||||
},
|
},
|
||||||
"PY": {
|
"PY": {
|
||||||
amount: 336000, // 35000 * 12 * 0.80 = Economia de 20%
|
amount: 336000, // 35000 * 12 * 0.80 = Economia de 20%
|
||||||
currency: "PYG",
|
currency: "PYG",
|
||||||
stripePriceId: "price_XXXXX_yearly_py" // UPDATE from appsettings.json > Stripe:Plans:Yearly:PY
|
stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi"
|
||||||
},
|
},
|
||||||
"US": {
|
"US": {
|
||||||
amount: 19.10, // 1.99 * 12 * 0.80 = Economia de 20%
|
amount: 19.10, // 1.99 * 12 * 0.80 = Economia de 20%
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
stripePriceId: "price_XXXXX_yearly_us" // UPDATE from appsettings.json > Stripe:Plans:Yearly:US
|
stripePriceId: "price_XXXXX_yearly_us" // TODO: Update with real Stripe Price ID
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
|||||||
63
Scripts/update-plans.sh
Normal file
63
Scripts/update-plans.sh
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to update MongoDB Plans via localhost-only API endpoint
|
||||||
|
# Usage: ./update-plans.sh [port]
|
||||||
|
|
||||||
|
PORT=${1:-52428}
|
||||||
|
API_URL="https://localhost:$PORT/api/Admin/SeedPlans"
|
||||||
|
PLANS_FILE="$(dirname "$0")/plans.json"
|
||||||
|
|
||||||
|
echo "🔧 QR Rapido - Update Plans Script"
|
||||||
|
echo "=================================="
|
||||||
|
echo ""
|
||||||
|
echo "📋 Plans file: $PLANS_FILE"
|
||||||
|
echo "🌐 API URL: $API_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if plans.json exists
|
||||||
|
if [ ! -f "$PLANS_FILE" ]; then
|
||||||
|
echo "❌ Error: plans.json not found at $PLANS_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "Please create plans.json with your Stripe Price IDs"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the app is running
|
||||||
|
echo "🔍 Checking if app is running on port $PORT..."
|
||||||
|
if ! curl -k -s "https://localhost:$PORT/health" > /dev/null 2>&1; then
|
||||||
|
echo "❌ Error: App not responding on https://localhost:$PORT"
|
||||||
|
echo ""
|
||||||
|
echo "Please start the app first:"
|
||||||
|
echo " dotnet run"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ App is running"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Send request
|
||||||
|
echo "📤 Sending plans to API..."
|
||||||
|
response=$(curl -k -s -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @"$PLANS_FILE" \
|
||||||
|
"$API_URL")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$http_code" -eq 200 ]; then
|
||||||
|
echo "✅ Success! Plans updated in MongoDB"
|
||||||
|
echo ""
|
||||||
|
echo "$body" | jq '.' 2>/dev/null || echo "$body"
|
||||||
|
else
|
||||||
|
echo "❌ Error! HTTP Status: $http_code"
|
||||||
|
echo ""
|
||||||
|
echo "$body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Done!"
|
||||||
@ -33,6 +33,24 @@ namespace QRRapidoApp.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var customerId = user.StripeCustomerId;
|
var customerId = user.StripeCustomerId;
|
||||||
|
var customerService = new CustomerService();
|
||||||
|
|
||||||
|
// Verify if customer exists in Stripe, create new if not
|
||||||
|
if (!string.IsNullOrEmpty(customerId))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to retrieve the customer to verify it exists
|
||||||
|
await customerService.GetAsync(customerId);
|
||||||
|
_logger.LogInformation($"Using existing Stripe customer {customerId} for user {userId}");
|
||||||
|
}
|
||||||
|
catch (StripeException ex) when (ex.StripeError?.Code == "resource_missing")
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Stripe customer {customerId} not found, creating new one for user {userId}");
|
||||||
|
customerId = null; // Force creation of new customer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(customerId))
|
if (string.IsNullOrEmpty(customerId))
|
||||||
{
|
{
|
||||||
var customerOptions = new CustomerCreateOptions
|
var customerOptions = new CustomerCreateOptions
|
||||||
@ -41,10 +59,10 @@ namespace QRRapidoApp.Services
|
|||||||
Name = user.Name,
|
Name = user.Name,
|
||||||
Metadata = new Dictionary<string, string> { { "app_user_id", user.Id } }
|
Metadata = new Dictionary<string, string> { { "app_user_id", user.Id } }
|
||||||
};
|
};
|
||||||
var customerService = new CustomerService();
|
|
||||||
var customer = await customerService.CreateAsync(customerOptions);
|
var customer = await customerService.CreateAsync(customerOptions);
|
||||||
customerId = customer.Id;
|
customerId = customer.Id;
|
||||||
await _userService.UpdateUserStripeCustomerIdAsync(userId, customerId);
|
await _userService.UpdateUserStripeCustomerIdAsync(userId, customerId);
|
||||||
|
_logger.LogInformation($"Created new Stripe customer {customerId} for user {userId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var options = new SessionCreateOptions
|
var options = new SessionCreateOptions
|
||||||
|
|||||||
@ -145,15 +145,9 @@
|
|||||||
else if (User.Identity.IsAuthenticated)
|
else if (User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
var isPremium = await AdService.HasValidPremiumSubscription(userId);
|
var isPremium = await AdService.HasValidPremiumSubscription(userId);
|
||||||
if (isPremium)
|
if (!isPremium)
|
||||||
{
|
|
||||||
<div class="alert alert-success ad-free-notice mb-3">
|
|
||||||
<i class="fas fa-crown text-warning"></i>
|
|
||||||
<span><strong>@Localizer["PremiumUserNoAds"]</strong></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
@* Only show upgrade notice for non-premium users *@
|
||||||
<div class="alert alert-info upgrade-notice mb-3">
|
<div class="alert alert-info upgrade-notice mb-3">
|
||||||
<i class="fas fa-star text-warning"></i>
|
<i class="fas fa-star text-warning"></i>
|
||||||
<span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span>
|
<span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span>
|
||||||
@ -162,4 +156,5 @@ else if (User.Identity.IsAuthenticated)
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@* Premium users: render nothing (100% invisible) *@
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,24 @@
|
|||||||
"GrowthRateWarningMBPerHour": 200,
|
"GrowthRateWarningMBPerHour": 200,
|
||||||
"IncludeCollectionStats": true
|
"IncludeCollectionStats": true
|
||||||
},
|
},
|
||||||
|
"Stripe": {
|
||||||
|
"PublishableKey": "pk_test_51Rs42SB6bFjHQirAJ6kzbFCbBuAobyNbmlgpULFsInl8KRzlpclUoqOZICqvp2S51kquw3Bc04CPO9bNUEgDLDgd00XbAHT7Fh",
|
||||||
|
"SecretKey": "sk_test_51Rs42SB6bFjHQirANOUg8jgzPALbNdVWULSVRMycFRBTzE0QUGA6pkpoQaTVsCIoV3XGRgoJ7E3CA6Y67vWlM76q00QBoKW0aH",
|
||||||
|
"WebhookSecret": "whsec_667402ff1d753b181f626636d556975f2749b5fec4d1231d44f040b057fb3009",
|
||||||
|
"ProductId": "prod_TGTbombliOUYmQ",
|
||||||
|
"Plans": {
|
||||||
|
"Monthly": {
|
||||||
|
"BR": "price_1SJwebB6bFjHQirAloEqXWd6",
|
||||||
|
"PY": "price_1SK4Y0B6bFjHQirAaxNHxILi",
|
||||||
|
"US": "price_XXXXX_monthly_us"
|
||||||
|
},
|
||||||
|
"Yearly": {
|
||||||
|
"BR": "price_1SK4X7B6bFjHQirAdMtviPw4",
|
||||||
|
"PY": "price_1SK4Y0B6bFjHQirAaxNHxILi",
|
||||||
|
"US": "price_XXXXX_yearly_us"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"HealthChecks": {
|
"HealthChecks": {
|
||||||
"MongoDB": {
|
"MongoDB": {
|
||||||
"TimeoutSeconds": 10,
|
"TimeoutSeconds": 10,
|
||||||
|
|||||||
@ -21,22 +21,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Stripe": {
|
"Stripe": {
|
||||||
"PublishableKey": "pk_test_51Rs42tBeR5IFYUsBooapyDwQTgh6CFuKbya5R3MVDTrdOUKmgiHQYipU0pgOdG5iKogH77RUYIKBJzbCt5BghUOY00xitV5KiN",
|
"PublishableKey": "pk_test_51Rs42SB6bFjHQirAJ6kzbFCbBuAobyNbmlgpULFsInl8KRzlpclUoqOZICqvp2S51kquw3Bc04CPO9bNUEgDLDgd00XbAHT7Fh",
|
||||||
"SecretKey": "sk_test_51Rs42tBeR5IFYUsBtycRlJJcdwgoMbh8MfQIKIGelBPTQFwDcOn2iCCbw5uG6hnqlpgNAUuFgWRAUUMA8qkABKun00EIx4odDF",
|
"SecretKey": "sk_test_51Rs42SB6bFjHQirANOUg8jgzPALbNdVWULSVRMycFRBTzE0QUGA6pkpoQaTVsCIoV3XGRgoJ7E3CA6Y67vWlM76q00QBoKW0aH",
|
||||||
"WebhookSecret": "whsec_2e828803ceb48e7865458b0cf332b68535fdff8753d26d69b1c88ea55cb0e482",
|
"WebhookSecret": "whsec_667402ff1d753b181f626636d556975f2749b5fec4d1231d44f040b057fb3009"
|
||||||
"ProductId": "prod_SnfQTxwE3i8r5L",
|
|
||||||
"Plans": {
|
|
||||||
"Monthly": {
|
|
||||||
"BR": "price_1Rs45OBeR5IFYUsBfsnOpOiv",
|
|
||||||
"PY": "price_XXXXX_monthly_py",
|
|
||||||
"US": "price_XXXXX_monthly_us"
|
|
||||||
},
|
|
||||||
"Yearly": {
|
|
||||||
"BR": "price_1Rs4AyBeR5IFYUsB8kRSNUIM",
|
|
||||||
"PY": "price_XXXXX_yearly_py",
|
|
||||||
"US": "price_XXXXX_yearly_us"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"AdSense": {
|
"AdSense": {
|
||||||
"ClientId": "ca-pub-3475956393038764",
|
"ClientId": "ca-pub-3475956393038764",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user