fix: ajuste de warnings e combos
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m50s
Deploy QR Rapido / build-and-push (push) Successful in 16m49s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s

This commit is contained in:
Ricardo Carneiro 2026-01-25 00:44:34 -03:00
parent 4a7cdbf26d
commit dbd3c8851b
12 changed files with 282 additions and 156 deletions

View File

@ -38,7 +38,7 @@ namespace QRRapidoApp.Configuration
{ {
try try
{ {
var secretValue = File.ReadAllText(secretFilePath).Trim(); var secretValue = (string?)File.ReadAllText(secretFilePath).Trim();
if (!string.IsNullOrEmpty(secretValue)) if (!string.IsNullOrEmpty(secretValue))
{ {
Data[configKey] = secretValue; Data[configKey] = secretValue;

View File

@ -170,7 +170,9 @@ namespace QRRapidoApp.Controllers
var user = await _userService.GetUserByProviderAsync(scheme, providerId); var user = await _userService.GetUserByProviderAsync(scheme, providerId);
if (user == null) if (user == null)
{ {
user = await _userService.CreateUserAsync(email, name ?? email, scheme, providerId); // Fix CS8625: Ensure name is not null
var safeName = !string.IsNullOrEmpty(name) ? name : (email ?? "User");
user = await _userService.CreateUserAsync(email, safeName, scheme, providerId);
} }
else else
{ {
@ -180,10 +182,10 @@ namespace QRRapidoApp.Controllers
// Create application claims // Create application claims
var claims = new List<Claim> var claims = new List<Claim>
{ {
new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.NameIdentifier, user.Id ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Email, user.Email ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Name, user.Name), new Claim(ClaimTypes.Name, user.Name ?? string.Empty), // Fix CS8625
new Claim("Provider", user.Provider), new Claim("Provider", user.Provider ?? string.Empty),
new Claim("IsPremium", user.IsPremium.ToString()) new Claim("IsPremium", user.IsPremium.ToString())
}; };
@ -230,6 +232,7 @@ namespace QRRapidoApp.Controllers
return RedirectToAction("Login"); return RedirectToAction("Login");
} }
// Ensure we are passing a non-null userId
ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10); ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId); ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId); ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId);

View File

@ -33,20 +33,68 @@ namespace QRRapidoApp.Controllers
_markdownService = markdownService; _markdownService = markdownService;
} }
public async Task<IActionResult> Index() // Default fallback route handled by Program.cs map
[HttpGet]
public async Task<IActionResult> Index(string? qrType = null)
{ {
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId); // Pass the requested QR type to the view to auto-select in combo
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? ""); ViewBag.SelectedQRType = qrType;
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
_adDisplayService.SetViewBagAds(ViewBag);
// SEO and Analytics data // Set SEO Meta Tags based on QR Type
if (!string.IsNullOrEmpty(qrType))
{
switch (qrType.ToLower())
{
case "pix":
ViewBag.Title = "Gerador de Pix Grátis";
ViewBag.Description = "Crie QR Code PIX estático gratuitamente. Ideal para receber pagamentos e doações.";
ViewBag.Keywords = "pix, qr code pix, gerador pix, pix estatico, receber pix";
break;
case "wifi":
ViewBag.Title = _localizer["WiFiQRTitle"];
ViewBag.Description = _localizer["WiFiQRDescription"];
break;
case "vcard":
ViewBag.Title = _localizer["VCardQRTitle"] ?? "Gerador de Cartão de Visita Digital";
ViewBag.Description = _localizer["VCardQRDescription"];
break;
case "whatsapp":
ViewBag.Title = "Gerador de Link WhatsApp";
ViewBag.Description = _localizer["WhatsAppQRDescription"];
break;
case "email":
ViewBag.Title = "Gerador de QR Code Email";
ViewBag.Description = _localizer["EmailQRDescription"];
break;
case "sms":
ViewBag.Title = "Gerador de QR Code SMS";
ViewBag.Description = _localizer["SMSQRDescription"];
break;
case "text":
ViewBag.Title = "Gerador de Texto para QR";
ViewBag.Description = _localizer["TextQRDescription"];
break;
case "url":
ViewBag.Title = "Gerador de URL para QR";
ViewBag.Description = _localizer["URLQRDescription"];
break;
}
}
else
{
// Default SEO
ViewBag.Title = _config["App:TaglinePT"]; ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"]; ViewBag.Keywords = _config["SEO:KeywordsPT"];
ViewBag.Description = _localizer["QRGenerateDescription"]; ViewBag.Description = _localizer["QRGenerateDescription"];
}
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
ViewBag.IsAuthenticated = User?.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User?.Identity?.Name ?? "";
_adDisplayService.SetViewBagAds(ViewBag);
// User stats for logged in users // User stats for logged in users
if (!string.IsNullOrEmpty(userId)) if (!string.IsNullOrEmpty(userId))
@ -55,9 +103,42 @@ namespace QRRapidoApp.Controllers
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId); ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
} }
return View(); return View("Index");
} }
// Dedicated SEO Routes - These act as virtual pages
[Route("pix")]
[Route("{culture}/pix")]
public async Task<IActionResult> Pix() => await Index("pix");
[Route("wifi")]
[Route("{culture}/wifi")]
public async Task<IActionResult> Wifi() => await Index("wifi");
[Route("vcard")]
[Route("{culture}/vcard")]
public async Task<IActionResult> VCard() => await Index("vcard");
[Route("whatsapp")]
[Route("{culture}/whatsapp")]
public async Task<IActionResult> WhatsApp() => await Index("whatsapp");
[Route("email")]
[Route("{culture}/email")]
public async Task<IActionResult> Email() => await Index("email");
[Route("sms")]
[Route("{culture}/sms")]
public async Task<IActionResult> Sms() => await Index("sms");
[Route("text")]
[Route("{culture}/text")]
public async Task<IActionResult> Text() => await Index("text");
[Route("url")]
[Route("{culture}/url")]
public async Task<IActionResult> UrlType() => await Index("url");
public IActionResult Privacy() public IActionResult Privacy()
{ {
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
@ -238,7 +319,18 @@ namespace QRRapidoApp.Controllers
AppendUrl("/pt-BR", "daily", "0.9"); AppendUrl("/pt-BR", "daily", "0.9");
AppendUrl("/es-PY", "daily", "0.9"); AppendUrl("/es-PY", "daily", "0.9");
// Tools (Virtual Pages)
var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "text", "url" };
var cultures = new[] { "pt-BR", "es-PY" }; var cultures = new[] { "pt-BR", "es-PY" };
foreach (var culture in cultures)
{
foreach (var tool in tools)
{
AppendUrl($"/{culture}/{tool}", "weekly", "0.9");
}
}
var informationalPages = new[] var informationalPages = new[]
{ {
new { Path = "Home/About", ChangeFreq = "monthly", Priority = "0.8" }, new { Path = "Home/About", ChangeFreq = "monthly", Priority = "0.8" },

View File

@ -40,12 +40,8 @@ namespace QRRapidoApp.Middleware
var detectedCulture = DetectBrowserLanguage(context); var detectedCulture = DetectBrowserLanguage(context);
// If the detected culture is the default, do not redirect. // ALWAYS Redirect to include culture in path for consistency and SEO
if (detectedCulture == DefaultCulture) // This ensures /pix becomes /pt-BR/pix or /es-PY/pix
{
await _next(context);
return;
}
var redirectUrl = $"/{detectedCulture}"; var redirectUrl = $"/{detectedCulture}";
if (!string.IsNullOrEmpty(path)) if (!string.IsNullOrEmpty(path))

View File

@ -30,22 +30,10 @@ using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using AspNetCore.DataProtection.MongoDb; using AspNetCore.DataProtection.MongoDb;
// Fix for WSL path issues - disable StaticWebAssets completely // Disable StaticWebAssets for WSL compatibility (keeping this as it might still be needed for your mixed env)
var options = new WebApplicationOptions
{
Args = args,
ContentRootPath = Directory.GetCurrentDirectory(),
WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")
};
// Disable StaticWebAssets for WSL compatibility
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED", "false"); Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED", "false");
var builder = WebApplication.CreateBuilder(options); var builder = WebApplication.CreateBuilder(args);
// Add Docker Secrets as configuration source (for Swarm deployments)
// Secrets override values from appsettings.json
builder.Configuration.AddDockerSecrets();
// Configure Serilog // Configure Serilog
var loggerConfig = new LoggerConfiguration() var loggerConfig = new LoggerConfiguration()

View File

@ -22,7 +22,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.OpenSearch" Version="1.3.0" /> <PackageReference Include="Serilog.Sinks.OpenSearch" Version="1.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
<PackageReference Include="Stripe.net" Version="48.4.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" />

View File

@ -760,6 +760,9 @@
<data name="FastestQRGeneratorDescription" xml:space="preserve"> <data name="FastestQRGeneratorDescription" xml:space="preserve">
<value>El generador de códigos QR pya'eve (ás rápido) de la web. Rei rehe (Gratis), seguro y confiable.</value> <value>El generador de códigos QR pya'eve (ás rápido) de la web. Rei rehe (Gratis), seguro y confiable.</value>
</data> </data>
<data name="Tools" xml:space="preserve">
<value>Herramientas</value>
</data>
<data name="UsefulLinks" xml:space="preserve"> <data name="UsefulLinks" xml:space="preserve">
<value>Enlaces Útiles</value> <value>Enlaces Útiles</value>
</data> </data>

View File

@ -813,6 +813,9 @@
<data name="FastestQRGeneratorDescription" xml:space="preserve"> <data name="FastestQRGeneratorDescription" xml:space="preserve">
<value>O gerador de QR codes mais rápido da web. Grátis, seguro e confiável.</value> <value>O gerador de QR codes mais rápido da web. Grátis, seguro e confiável.</value>
</data> </data>
<data name="Tools" xml:space="preserve">
<value>Ferramentas</value>
</data>
<data name="UsefulLinks" xml:space="preserve"> <data name="UsefulLinks" xml:space="preserve">
<value>Links Úteis</value> <value>Links Úteis</value>
</data> </data>

View File

@ -97,24 +97,38 @@ namespace QRRapidoApp.Services
switch (stripeEvent.Type) switch (stripeEvent.Type)
{ {
case "checkout.session.completed": case "checkout.session.completed":
var session = stripeEvent.Data.Object as Session; if (stripeEvent.Data.Object is Session session)
if (session?.SubscriptionId != null) {
if (session.SubscriptionId != null)
{ {
var subscriptionService = new SubscriptionService(); var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(session.SubscriptionId); var subscription = await subscriptionService.GetAsync(session.SubscriptionId);
await ProcessSubscriptionActivation(session.ClientReferenceId, subscription); // Fix CS8604: Ensure ClientReferenceId is not null
var userId = session.ClientReferenceId ??
(session.Metadata != null && session.Metadata.ContainsKey("user_id") ? session.Metadata["user_id"] : null);
if (!string.IsNullOrEmpty(userId))
{
await ProcessSubscriptionActivation(userId, subscription);
}
else
{
_logger.LogWarning($"Missing userId in checkout session {session.Id}");
}
}
} }
break; break;
case "invoice.finalized": case "invoice.finalized":
var invoice = stripeEvent.Data.Object as Invoice; if (stripeEvent.Data.Object is Invoice invoice)
{
var subscriptionLineItem = invoice.Lines?.Data var subscriptionLineItem = invoice.Lines?.Data
.FirstOrDefault(line => .FirstOrDefault(line =>
!string.IsNullOrEmpty(line.SubscriptionId) || !string.IsNullOrEmpty(line.SubscriptionId) ||
line.Subscription != null line.Subscription != null
); );
string subscriptionId = null; string? subscriptionId = null;
if (subscriptionLineItem != null) if (subscriptionLineItem != null)
{ {
@ -133,11 +147,11 @@ namespace QRRapidoApp.Services
await ProcessSubscriptionActivation(user.Id, subscription); await ProcessSubscriptionActivation(user.Id, subscription);
} }
} }
}
break; break;
case "customer.subscription.deleted": case "customer.subscription.deleted":
var deletedSubscription = stripeEvent.Data.Object as Subscription; if (stripeEvent.Data.Object is Subscription deletedSubscription)
if (deletedSubscription != null)
{ {
await _userService.DeactivatePremiumStatus(deletedSubscription.Id); await _userService.DeactivatePremiumStatus(deletedSubscription.Id);
} }

View File

@ -1580,6 +1580,24 @@
} }
}); });
} }
// Auto-select QR Type from Server Route (SEO)
const serverSelectedType = '@(ViewBag.SelectedQRType ?? "")';
if (serverSelectedType) {
const qrTypeSelect = document.getElementById('qr-type');
if (qrTypeSelect) {
// Find option with this value (case-insensitive)
const option = Array.from(qrTypeSelect.options).find(opt =>
opt.value.toLowerCase() === serverSelectedType.toLowerCase()
);
if (option) {
qrTypeSelect.value = option.value;
// Trigger change event to load correct interface
qrTypeSelect.dispatchEvent(new Event('change'));
}
}
}
}); });
</script> </script>
} }

View File

@ -404,15 +404,24 @@
<h5>QR Rapido</h5> <h5>QR Rapido</h5>
<p class="small">@Localizer["FastestQRGeneratorDescription"]</p> <p class="small">@Localizer["FastestQRGeneratorDescription"]</p>
</div> </div>
<div class="col-md-3"> <div class="col-md-2">
<h6>@Localizer["Tools"]</h6>
<ul class="list-unstyled small">
<li><a href="/pix" class="text-light text-decoration-none">Gerador de PIX</a></li>
<li><a href="/wifi" class="text-light text-decoration-none">QR Code WiFi</a></li>
<li><a href="/whatsapp" class="text-light text-decoration-none">Link WhatsApp</a></li>
<li><a href="/vcard" class="text-light text-decoration-none">Cartão Digital</a></li>
</ul>
</div>
<div class="col-md-2">
<h6>@Localizer["UsefulLinks"]</h6> <h6>@Localizer["UsefulLinks"]</h6>
<ul class="list-unstyled"> <ul class="list-unstyled small">
<li><a href="@Url.Action("Privacy", "Home")" class="text-light">@Localizer["Privacy"]</a></li> <li><a href="@Url.Action("Privacy", "Home")" class="text-light">@Localizer["Privacy"]</a></li>
<li><a href="@Url.Action("Terms", "Home")" class="text-light">@Localizer["TermsOfUse"]</a></li> <li><a href="@Url.Action("Terms", "Home")" class="text-light">@Localizer["TermsOfUse"]</a></li>
<li><a href="@Url.Action("Upgrade", "Premium")" class="text-warning">Premium</a></li> <li><a href="@Url.Action("Upgrade", "Premium")" class="text-warning">Premium</a></li>
</ul> </ul>
</div> </div>
<div class="col-md-3"> <div class="col-md-2">
<h6>@Localizer["Support"]</h6> <h6>@Localizer["Support"]</h6>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li><a href="mailto:contato@qrrapido.site" class="text-light">Contato</a></li> <li><a href="mailto:contato@qrrapido.site" class="text-light">Contato</a></li>

View File

@ -1994,8 +1994,8 @@ class QRRapidoGenerator {
const urlPreview = document.getElementById('url-preview'); const urlPreview = document.getElementById('url-preview');
// Helper to safely hide // Helper to safely hide
const safeHide = (el) => { if (el) el.style.display = 'none'; }; const safeHide = (el) => { if (el) { el.style.display = 'none'; el.classList.add('disabled-state'); } };
const safeShow = (el) => { if (el) el.style.display = 'block'; }; const safeShow = (el) => { if (el) { el.style.display = 'block'; el.classList.remove('disabled-state'); } };
// 1. Hide EVERYTHING specific first // 1. Hide EVERYTHING specific first
safeHide(vcardInterface); safeHide(vcardInterface);