first commit
Some checks failed
Deploy QR Rapido / test (push) Successful in 4m58s
Deploy QR Rapido / build-and-push (push) Failing after 1m39s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped

This commit is contained in:
Ricardo Carneiro 2025-07-28 11:46:48 -03:00
parent da57bb8dd8
commit 2ccd35bb7d
44 changed files with 6996 additions and 0 deletions

View File

@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(dotnet new:*)",
"Bash(find:*)",
"Bash(dotnet build:*)"
],
"deny": []
}
}

121
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,121 @@
name: Deploy QR Rapido
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage"
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: coverage.cobertura.xml
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- name: Deploy to Staging
uses: azure/webapps-deploy@v2
with:
app-name: 'qrrapido-staging'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_STAGING }}
images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop'
deploy-production:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to Production
uses: azure/webapps-deploy@v2
with:
app-name: 'qrrapido-production'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_PROD }}
images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest'
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
text: "QR Rapido deployed to production! 🚀"

382
.gitignore vendored Normal file
View File

@ -0,0 +1,382 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/*.HxS
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these files may be disclosed. Comment the next line if you want to checkin
# your web deploy settings, but sensitive information contained in these files may
# be disclosed. Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.azurePubxml
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment the next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
CConvertLog*.txt
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# Custom
appsettings.Local.json
appsettings.Production.json
*.Development.json
logs/
uploads/
temp/
*.env
.env.local
.env.production
# macOS
.DS_Store
# IDE
.vscode/
.idea/
*.swp
*.swo
# Database
*.db
*.sqlite
*.sqlite3
# Certificates
*.pfx
*.crt
*.key

View File

@ -0,0 +1,242 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
public class AccountController : Controller
{
private readonly IUserService _userService;
private readonly AdDisplayService _adDisplayService;
private readonly ILogger<AccountController> _logger;
public AccountController(IUserService userService, AdDisplayService adDisplayService, ILogger<AccountController> logger)
{
_userService = userService;
_adDisplayService = adDisplayService;
_logger = logger;
}
[HttpGet]
public IActionResult Login(string returnUrl = "/")
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpGet]
public IActionResult LoginGoogle(string returnUrl = "/")
{
var properties = new AuthenticationProperties
{
RedirectUri = Url.Action("GoogleCallback"),
Items = { { "returnUrl", returnUrl } }
};
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult LoginMicrosoft(string returnUrl = "/")
{
var properties = new AuthenticationProperties
{
RedirectUri = Url.Action("MicrosoftCallback"),
Items = { { "returnUrl", returnUrl } }
};
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
}
[HttpGet]
public async Task<IActionResult> GoogleCallback()
{
return await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme);
}
[HttpGet]
public async Task<IActionResult> MicrosoftCallback()
{
return await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme);
}
private async Task<IActionResult> HandleExternalLoginCallbackAsync(string scheme)
{
try
{
var result = await HttpContext.AuthenticateAsync(scheme);
if (!result.Succeeded)
{
_logger.LogWarning($"External authentication failed for scheme {scheme}");
return RedirectToAction("Login");
}
var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value;
var name = result.Principal?.FindFirst(ClaimTypes.Name)?.Value;
var providerId = result.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId))
{
_logger.LogWarning($"Missing required claims from {scheme} authentication");
return RedirectToAction("Login");
}
// Find or create user
var user = await _userService.GetUserByProviderAsync(scheme, providerId);
if (user == null)
{
user = await _userService.CreateUserAsync(email, name ?? email, scheme, providerId);
}
else
{
await _userService.UpdateLastLoginAsync(user.Id);
}
// Create application claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, user.Name),
new Claim("Provider", user.Provider),
new Claim("IsPremium", user.IsPremium.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30)
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity), authProperties);
var returnUrl = result.Properties?.Items["returnUrl"] ?? "/";
return Redirect(returnUrl);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in external login callback for {scheme}");
return RedirectToAction("Login");
}
}
[HttpPost]
[Authorize]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index", "Home");
}
[HttpGet]
[Authorize]
public async Task<IActionResult> Profile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var user = await _userService.GetUserAsync(userId);
if (user == null)
{
return RedirectToAction("Login");
}
ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
return View(user);
}
[HttpGet]
public async Task<IActionResult> AdFreeStatus()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new AdFreeStatusViewModel
{
IsAdFree = false,
TimeRemaining = 0,
IsPremium = false
});
}
var shouldShowAds = await _adDisplayService.ShouldShowAds(userId);
var isPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
var expiryDate = await _adDisplayService.GetAdFreeExpiryDate(userId);
var status = await _adDisplayService.GetAdFreeStatusAsync(userId);
return Json(new AdFreeStatusViewModel
{
IsAdFree = !shouldShowAds,
TimeRemaining = isPremium ? int.MaxValue : 0,
IsPremium = isPremium,
ExpiryDate = expiryDate,
SessionType = status
});
}
[HttpPost]
[Authorize]
public async Task<IActionResult> ExtendAdFreeTime(int minutes)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Método removido - sem extensão de tempo ad-free
return Json(new { success = false, message = "Feature not available" });
}
[HttpGet]
[Authorize]
public async Task<IActionResult> History()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var history = await _userService.GetUserQRHistoryAsync(userId, 50);
return View(history);
}
[HttpPost]
[Authorize]
public async Task<IActionResult> UpdatePreferences(string language)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false });
}
try
{
var user = await _userService.GetUserAsync(userId);
if (user != null)
{
user.PreferredLanguage = language;
await _userService.UpdateUserAsync(user);
return Json(new { success = true });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating preferences for user {userId}");
}
return Json(new { success = false });
}
}
}

View File

@ -0,0 +1,128 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models;
using QRRapidoApp.Services;
using System.Diagnostics;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AdDisplayService _adDisplayService;
private readonly IUserService _userService;
private readonly IConfiguration _config;
public HomeController(ILogger<HomeController> logger, AdDisplayService adDisplayService, IUserService userService, IConfiguration config)
{
_logger = logger;
_adDisplayService = adDisplayService;
_userService = userService;
_config = config;
}
public async Task<IActionResult> Index()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
// SEO and Analytics data
ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"];
ViewBag.Description = "QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.";
// User stats for logged in users
if (!string.IsNullOrEmpty(userId))
{
ViewBag.DailyQRCount = await _userService.GetDailyQRCountAsync(userId);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
}
return View();
}
public IActionResult Privacy()
{
return View();
}
public IActionResult Terms()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
// Dynamic QR redirect endpoint
[Route("d/{id}")]
public async Task<IActionResult> DynamicRedirect(string id)
{
try
{
// This would lookup the dynamic QR content from cache/database
// For now, return a placeholder
return Redirect("https://qrrapido.site");
}
catch
{
return NotFound();
}
}
// Health check endpoint
[Route("health")]
public IActionResult Health()
{
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
}
// Sitemap endpoint for SEO
[Route("sitemap.xml")]
public IActionResult Sitemap()
{
var sitemap = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
<url>
<loc>https://qrrapido.site/</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://qrrapido.site/pt/</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://qrrapido.site/es/</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://qrrapido.site/Premium/Upgrade</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>";
return Content(sitemap, "application/xml");
}
}
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

View File

@ -0,0 +1,230 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
[Authorize]
public class PremiumController : Controller
{
private readonly StripeService _stripeService;
private readonly IUserService _userService;
private readonly AdDisplayService _adDisplayService;
private readonly IConfiguration _config;
private readonly ILogger<PremiumController> _logger;
public PremiumController(StripeService stripeService, IUserService userService, AdDisplayService adDisplayService, IConfiguration config, ILogger<PremiumController> logger)
{
_stripeService = stripeService;
_userService = userService;
_adDisplayService = adDisplayService;
_config = config;
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> Upgrade()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login", "Account");
}
var user = await _userService.GetUserAsync(userId);
if (user?.IsPremium == true)
{
return RedirectToAction("Dashboard");
}
var model = new UpgradeViewModel
{
CurrentPlan = "Free",
PremiumPrice = _config.GetValue<decimal>("Premium:PremiumPrice"),
Features = _config.GetSection("Premium:Features").Get<Dictionary<string, bool>>() ?? new(),
RemainingQRs = await _userService.GetDailyQRCountAsync(userId),
IsAdFreeActive = !await _adDisplayService.ShouldShowAds(userId),
DaysUntilAdExpiry = (int)((await _adDisplayService.GetAdFreeExpiryDate(userId) - DateTime.UtcNow)?.TotalDays ?? 0)
};
return View(model);
}
[HttpPost]
public async Task<IActionResult> CreateCheckout()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false, error = "User not authenticated" });
}
var priceId = _config["Stripe:PriceId"];
if (string.IsNullOrEmpty(priceId))
{
return Json(new { success = false, error = "Stripe not configured" });
}
try
{
var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId);
return Json(new { success = true, url = checkoutUrl });
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error creating checkout session for user {userId}");
return Json(new { success = false, error = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> Success(string session_id)
{
if (string.IsNullOrEmpty(session_id))
{
return RedirectToAction("Upgrade");
}
try
{
ViewBag.Success = true;
ViewBag.SessionId = session_id;
return View();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing successful payment for session {session_id}");
return RedirectToAction("Upgrade");
}
}
[HttpGet]
public IActionResult Cancel()
{
ViewBag.Cancelled = true;
return View("Upgrade");
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> StripeWebhook()
{
try
{
using var reader = new StreamReader(HttpContext.Request.Body);
var json = await reader.ReadToEndAsync();
var signature = Request.Headers["Stripe-Signature"].FirstOrDefault();
if (string.IsNullOrEmpty(signature))
{
return BadRequest("Missing Stripe signature");
}
await _stripeService.HandleWebhookAsync(json, signature);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Stripe webhook");
return BadRequest(ex.Message);
}
}
[HttpGet]
public async Task<IActionResult> Dashboard()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login", "Account");
}
var user = await _userService.GetUserAsync(userId);
if (user?.IsPremium != true)
{
return RedirectToAction("Upgrade");
}
var model = new PremiumDashboardViewModel
{
User = user,
QRCodesThisMonth = await _userService.GetQRCountThisMonthAsync(userId),
TotalQRCodes = user.QRHistoryIds?.Count ?? 0,
SubscriptionStatus = await _stripeService.GetSubscriptionStatusAsync(user.StripeSubscriptionId),
RecentQRCodes = await _userService.GetUserQRHistoryAsync(userId, 10)
};
// Calculate next billing date (simplified)
if (user.PremiumExpiresAt.HasValue)
{
model.NextBillingDate = user.PremiumExpiresAt.Value.AddDays(-2); // Approximate
}
// QR type statistics
model.QRTypeStats = model.RecentQRCodes
.GroupBy(q => q.Type)
.ToDictionary(g => g.Key, g => g.Count());
return View(model);
}
[HttpPost]
public async Task<IActionResult> CancelSubscription()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false, error = "User not authenticated" });
}
try
{
var user = await _userService.GetUserAsync(userId);
if (user?.StripeSubscriptionId == null)
{
return Json(new { success = false, error = "No active subscription found" });
}
var success = await _stripeService.CancelSubscriptionAsync(user.StripeSubscriptionId);
if (success)
{
TempData["Success"] = "Assinatura cancelada com sucesso. Você manterá o acesso premium até o final do período pago.";
return Json(new { success = true });
}
else
{
return Json(new { success = false, error = "Failed to cancel subscription" });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error canceling subscription for user {userId}");
return Json(new { success = false, error = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> BillingPortal()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login", "Account");
}
try
{
// This would create a Stripe billing portal session
// For now, redirect to dashboard
return RedirectToAction("Dashboard");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error creating billing portal for user {userId}");
return RedirectToAction("Dashboard");
}
}
}
}

216
Controllers/QRController.cs Normal file
View File

@ -0,0 +1,216 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
using System.Diagnostics;
using System.Security.Claims;
using System.Text;
namespace QRRapidoApp.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class QRController : ControllerBase
{
private readonly IQRCodeService _qrService;
private readonly IUserService _userService;
private readonly AdDisplayService _adService;
private readonly ILogger<QRController> _logger;
public QRController(IQRCodeService qrService, IUserService userService, AdDisplayService adService, ILogger<QRController> logger)
{
_qrService = qrService;
_userService = userService;
_adService = adService;
_logger = logger;
}
[HttpPost("GenerateRapid")]
public async Task<IActionResult> GenerateRapid([FromBody] QRGenerationRequest request)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Quick validations
if (string.IsNullOrWhiteSpace(request.Content))
{
return BadRequest(new { error = "Conteúdo é obrigatório", success = false });
}
if (request.Content.Length > 4000) // Limit to maintain speed
{
return BadRequest(new { error = "Conteúdo muito longo. Máximo 4000 caracteres.", success = false });
}
// Check user status
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var user = await _userService.GetUserAsync(userId);
// Rate limiting for free users
if (!await CheckRateLimitAsync(userId, user))
{
return StatusCode(429, new
{
error = "Limite de QR codes atingido",
upgradeUrl = "/Premium/Upgrade",
success = false
});
}
// Configure optimizations based on user
request.IsPremium = user?.IsPremium == true;
request.OptimizeForSpeed = true;
// Generate QR code
var result = await _qrService.GenerateRapidAsync(request);
if (!result.Success)
{
return StatusCode(500, new { error = result.ErrorMessage, success = false });
}
// Update counter for free users
if (!request.IsPremium && userId != null)
{
var remaining = await _userService.DecrementDailyQRCountAsync(userId);
result.RemainingQRs = remaining;
}
// Save to history if user is logged in (fire and forget)
if (userId != null)
{
_ = Task.Run(async () =>
{
try
{
await _userService.SaveQRToHistoryAsync(userId, result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving QR to history");
}
});
}
stopwatch.Stop();
// Performance logging
_logger.LogInformation($"QR Rapido generated in {stopwatch.ElapsedMilliseconds}ms " +
$"(service: {result.GenerationTimeMs}ms, " +
$"cache: {result.FromCache}, " +
$"user: {(request.IsPremium ? "premium" : "free")})");
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in rapid QR code generation");
return StatusCode(500, new { error = "Erro interno do servidor", success = false });
}
}
[HttpGet("Download/{qrId}")]
public async Task<IActionResult> Download(string qrId, string format = "png")
{
try
{
var qrData = await _userService.GetQRDataAsync(qrId);
if (qrData == null)
{
return NotFound();
}
var contentType = format.ToLower() switch
{
"svg" => "image/svg+xml",
"pdf" => "application/pdf",
_ => "image/png"
};
var fileName = $"qrrapido-{DateTime.Now:yyyyMMdd-HHmmss}.{format}";
if (format.ToLower() == "svg")
{
var svgContent = await _qrService.ConvertToSvgAsync(qrData.QRCodeBase64);
return File(svgContent, contentType, fileName);
}
else if (format.ToLower() == "pdf")
{
var pdfContent = await _qrService.ConvertToPdfAsync(qrData.QRCodeBase64, qrData.Size);
return File(pdfContent, contentType, fileName);
}
var imageBytes = Convert.FromBase64String(qrData.QRCodeBase64);
return File(imageBytes, contentType, fileName);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error downloading QR {qrId}");
return StatusCode(500);
}
}
[HttpPost("SaveToHistory")]
public async Task<IActionResult> SaveToHistory([FromBody] SaveToHistoryRequest request)
{
try
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized();
}
var qrData = await _userService.GetQRDataAsync(request.QrId);
if (qrData == null)
{
return NotFound();
}
// QR is already saved when generated, just return success
return Ok(new { success = true, message = "QR Code salvo no histórico!" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving QR to history");
return StatusCode(500, new { error = "Erro ao salvar no histórico." });
}
}
[HttpGet("History")]
public async Task<IActionResult> GetHistory(int limit = 20)
{
try
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized();
}
var history = await _userService.GetUserQRHistoryAsync(userId, limit);
return Ok(history);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting QR history");
return StatusCode(500);
}
}
private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
{
if (user?.IsPremium == true) return true;
var dailyLimit = userId != null ? 50 : 10; // Logged in: 50/day, Anonymous: 10/day
var currentCount = await _userService.GetDailyQRCountAsync(userId);
return currentCount < dailyLimit;
}
}
public class SaveToHistoryRequest
{
public string QrId { get; set; } = string.Empty;
}
}

94
Data/MongoDbContext.cs Normal file
View File

@ -0,0 +1,94 @@
using MongoDB.Driver;
using QRRapidoApp.Models;
namespace QRRapidoApp.Data
{
public class MongoDbContext
{
private readonly IMongoDatabase _database;
private readonly bool _isConnected;
public MongoDbContext(IConfiguration configuration, IMongoClient mongoClient = null)
{
var connectionString = configuration.GetConnectionString("MongoDB");
if (mongoClient != null && !string.IsNullOrEmpty(connectionString))
{
try
{
var databaseName = MongoUrl.Create(connectionString).DatabaseName;
_database = mongoClient.GetDatabase(databaseName);
_isConnected = true;
}
catch
{
_isConnected = false;
}
}
else
{
_isConnected = false;
}
}
public IMongoCollection<User> Users => _isConnected ? _database.GetCollection<User>("users") : null;
public IMongoCollection<QRCodeHistory> QRCodeHistory => _isConnected ? _database.GetCollection<QRCodeHistory>("qr_codes") : null;
public IMongoCollection<AdFreeSession> AdFreeSessions => _isConnected ? _database.GetCollection<AdFreeSession>("ad_free_sessions") : null;
public bool IsConnected => _isConnected;
public async Task InitializeAsync()
{
if (_isConnected)
{
// Create indexes for better performance
await CreateIndexesAsync();
}
}
private async Task CreateIndexesAsync()
{
// User indexes
var userIndexKeys = Builders<User>.IndexKeys;
await Users.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.Email)),
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.ProviderId)),
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.Provider)),
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.LastLoginAt)),
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.IsPremium))
});
// QR Code History indexes
var qrIndexKeys = Builders<QRCodeHistory>.IndexKeys;
await QRCodeHistory.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.UserId)),
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.CreatedAt)),
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.Type)),
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.IsActive)),
new CreateIndexModel<QRCodeHistory>(
Builders<QRCodeHistory>.IndexKeys.Combine(
qrIndexKeys.Ascending(q => q.UserId),
qrIndexKeys.Descending(q => q.CreatedAt)
)
)
});
// Ad Free Session indexes
var adFreeIndexKeys = Builders<AdFreeSession>.IndexKeys;
await AdFreeSessions.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<AdFreeSession>(adFreeIndexKeys.Ascending(a => a.UserId)),
new CreateIndexModel<AdFreeSession>(adFreeIndexKeys.Ascending(a => a.ExpiresAt)),
new CreateIndexModel<AdFreeSession>(adFreeIndexKeys.Ascending(a => a.IsActive)),
new CreateIndexModel<AdFreeSession>(
Builders<AdFreeSession>.IndexKeys.Combine(
adFreeIndexKeys.Ascending(a => a.UserId),
adFreeIndexKeys.Ascending(a => a.IsActive),
adFreeIndexKeys.Descending(a => a.ExpiresAt)
)
)
});
}
}
}

54
Dockerfile Normal file
View File

@ -0,0 +1,54 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy csproj and restore dependencies (better caching)
COPY ["QRRapidoApp.csproj", "./"]
RUN dotnet restore "QRRapidoApp.csproj"
# Copy source code
COPY . .
# Build optimized for production
RUN dotnet build "QRRapidoApp.csproj" -c Release -o /app/build --no-restore
# Publish stage
FROM build AS publish
RUN dotnet publish "QRRapidoApp.csproj" -c Release -o /app/publish --no-restore --no-build \
/p:PublishReadyToRun=true \
/p:PublishSingleFile=false \
/p:PublishTrimmed=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# Install dependencies for QR code generation
RUN apt-get update && apt-get install -y \
libgdiplus \
libc6-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy application
COPY --from=publish /app/publish .
# Configure production environment
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:80
ENV DOTNET_EnableDiagnostics=0
# Create non-root user for security
RUN addgroup --system --gid 1001 qrrapido
RUN adduser --system --uid 1001 qrrapido
# Set ownership
RUN chown -R qrrapido:qrrapido /app
USER qrrapido
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
EXPOSE 80
ENTRYPOINT ["dotnet", "QRRapidoApp.dll"]

View File

@ -0,0 +1,48 @@
using QRRapidoApp.Services;
using System.Security.Claims;
namespace QRRapidoApp.Middleware
{
public class LastLoginUpdateMiddleware
{
private readonly RequestDelegate _next;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<LastLoginUpdateMiddleware> _logger;
public LastLoginUpdateMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory, ILogger<LastLoginUpdateMiddleware> logger)
{
_next = next;
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Only update login for authenticated users on first request of session
if (context.User.Identity?.IsAuthenticated == true)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userId) && !context.Session.GetString("LoginUpdated")?.Equals("true") == true)
{
try
{
using var scope = _scopeFactory.CreateScope();
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
await userService.UpdateLastLoginAsync(userId);
context.Session.SetString("LoginUpdated", "true");
_logger.LogInformation($"Updated last login for user {userId}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating last login for user {userId}: {ex.Message}");
}
}
}
await _next(context);
}
}
}

33
Models/AdFreeSession.cs Normal file
View File

@ -0,0 +1,33 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace QRRapidoApp.Models
{
public class AdFreeSession
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = string.Empty;
[BsonElement("userId")]
public string UserId { get; set; } = string.Empty;
[BsonElement("startedAt")]
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
[BsonElement("expiresAt")]
public DateTime ExpiresAt { get; set; }
[BsonElement("isActive")]
public bool IsActive { get; set; } = true;
[BsonElement("sessionType")]
public string SessionType { get; set; } = "Login"; // "Login", "Premium", "Trial", "Promotion"
[BsonElement("durationMinutes")]
public int DurationMinutes { get; set; } = 43200; // 30 days default
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

54
Models/QRCodeHistory.cs Normal file
View File

@ -0,0 +1,54 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace QRRapidoApp.Models
{
public class QRCodeHistory
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = string.Empty;
[BsonElement("userId")]
public string? UserId { get; set; } // null for anonymous users
[BsonElement("type")]
public string Type { get; set; } = string.Empty; // URL, Text, WiFi, vCard, SMS, Email
[BsonElement("content")]
public string Content { get; set; } = string.Empty;
[BsonElement("qrCodeBase64")]
public string QRCodeBase64 { get; set; } = string.Empty;
[BsonElement("customizationSettings")]
public string CustomizationSettings { get; set; } = string.Empty; // JSON
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[BsonElement("language")]
public string Language { get; set; } = "pt-BR";
[BsonElement("scanCount")]
public int ScanCount { get; set; } = 0;
[BsonElement("isDynamic")]
public bool IsDynamic { get; set; } = false;
[BsonElement("size")]
public int Size { get; set; } = 300;
[BsonElement("generationTimeMs")]
public long GenerationTimeMs { get; set; } = 0;
[BsonElement("fromCache")]
public bool FromCache { get; set; } = false;
[BsonElement("isActive")]
public bool IsActive { get; set; } = true;
[BsonElement("lastAccessedAt")]
public DateTime LastAccessedAt { get; set; } = DateTime.UtcNow;
}
}

60
Models/User.cs Normal file
View File

@ -0,0 +1,60 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace QRRapidoApp.Models
{
public class User
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = string.Empty;
[BsonElement("email")]
public string Email { get; set; } = string.Empty;
[BsonElement("name")]
public string Name { get; set; } = string.Empty;
[BsonElement("provider")]
public string Provider { get; set; } = string.Empty; // Google, Microsoft
[BsonElement("providerId")]
public string ProviderId { get; set; } = string.Empty;
[BsonElement("isPremium")]
public bool IsPremium { get; set; } = false;
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[BsonElement("premiumExpiresAt")]
public DateTime? PremiumExpiresAt { get; set; }
[BsonElement("premiumCancelledAt")]
public DateTime? PremiumCancelledAt { get; set; } // ADICIONADO: Data do cancelamento
[BsonElement("lastLoginAt")]
public DateTime LastLoginAt { get; set; } = DateTime.UtcNow;
[BsonElement("qrHistoryIds")]
public List<string> QRHistoryIds { get; set; } = new();
[BsonElement("stripeCustomerId")]
public string? StripeCustomerId { get; set; }
[BsonElement("stripeSubscriptionId")]
public string? StripeSubscriptionId { get; set; }
[BsonElement("preferredLanguage")]
public string PreferredLanguage { get; set; } = "pt-BR";
[BsonElement("dailyQRCount")]
public int DailyQRCount { get; set; } = 0;
[BsonElement("lastQRDate")]
public DateTime LastQRDate { get; set; } = DateTime.UtcNow.Date;
[BsonElement("totalQRGenerated")]
public int TotalQRGenerated { get; set; } = 0;
}
}

View File

@ -0,0 +1,32 @@
namespace QRRapidoApp.Models.ViewModels
{
public class UpgradeViewModel
{
public string CurrentPlan { get; set; } = "Free";
public decimal PremiumPrice { get; set; }
public Dictionary<string, bool> Features { get; set; } = new();
public int RemainingQRs { get; set; }
public int DaysUntilAdExpiry { get; set; }
public bool IsAdFreeActive { get; set; }
}
public class PremiumDashboardViewModel
{
public User User { get; set; } = new();
public int QRCodesThisMonth { get; set; }
public int TotalQRCodes { get; set; }
public string SubscriptionStatus { get; set; } = string.Empty;
public DateTime? NextBillingDate { get; set; }
public List<QRCodeHistory> RecentQRCodes { get; set; } = new();
public Dictionary<string, int> QRTypeStats { get; set; } = new();
}
public class AdFreeStatusViewModel
{
public bool IsAdFree { get; set; }
public int TimeRemaining { get; set; } // minutes
public bool IsPremium { get; set; }
public DateTime? ExpiryDate { get; set; }
public string SessionType { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,32 @@
namespace QRRapidoApp.Models.ViewModels
{
public class QRGenerationRequest
{
public string Type { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string QuickStyle { get; set; } = "classic";
public string PrimaryColor { get; set; } = "#000000";
public string BackgroundColor { get; set; } = "#FFFFFF";
public int Size { get; set; } = 300;
public int Margin { get; set; } = 2;
public string CornerStyle { get; set; } = "square";
public bool OptimizeForSpeed { get; set; } = true;
public string Language { get; set; } = "pt-BR";
public bool IsPremium { get; set; } = false;
public bool HasLogo { get; set; } = false;
public byte[]? Logo { get; set; }
}
public class QRGenerationResult
{
public string QRCodeBase64 { get; set; } = string.Empty;
public string QRId { get; set; } = string.Empty;
public long GenerationTimeMs { get; set; }
public bool FromCache { get; set; }
public int Size { get; set; }
public QRGenerationRequest? RequestSettings { get; set; }
public int? RemainingQRs { get; set; } // For free users
public bool Success { get; set; } = true;
public string? ErrorMessage { get; set; }
}
}

179
Program.cs Normal file
View File

@ -0,0 +1,179 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Localization;
using MongoDB.Driver;
using QRRapidoApp.Data;
using QRRapidoApp.Middleware;
using QRRapidoApp.Services;
using StackExchange.Redis;
using Stripe;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllersWithViews();
// MongoDB Configuration - optional for development
var mongoConnectionString = builder.Configuration.GetConnectionString("MongoDB");
if (!string.IsNullOrEmpty(mongoConnectionString))
{
try
{
builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
{
return new MongoClient(mongoConnectionString);
});
builder.Services.AddScoped<MongoDbContext>();
}
catch
{
// MongoDB not available - services will handle gracefully
builder.Services.AddScoped<MongoDbContext>();
}
}
else
{
// Development mode without MongoDB
builder.Services.AddScoped<MongoDbContext>();
}
// Cache Configuration - use Redis if available, otherwise memory cache
var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
if (!string.IsNullOrEmpty(redisConnectionString))
{
try
{
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnectionString;
});
}
catch
{
// Fallback to memory cache if Redis fails
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, QRRapidoApp.Services.MemoryDistributedCacheWrapper>();
}
}
else
{
// Use memory cache when Redis is not configured
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
}
// Authentication Configuration
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
})
.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
})
.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
{
options.ClientId = builder.Configuration["Authentication:Microsoft:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"];
});
// Stripe Configuration
StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"];
// Localization
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("pt-BR"),
new CultureInfo("es"),
new CultureInfo("en")
};
options.DefaultRequestCulture = new RequestCulture("pt-BR");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders.Insert(0, new QueryStringRequestCultureProvider());
options.RequestCultureProviders.Insert(1, new CookieRequestCultureProvider());
});
// Custom Services
builder.Services.AddScoped<IQRCodeService, QRRapidoService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<AdDisplayService>();
builder.Services.AddScoped<StripeService>();
// Background Services
builder.Services.AddHostedService<HistoryCleanupService>();
// CORS for API endpoints
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigins", policy =>
{
policy.WithOrigins("https://qrrapido.site", "https://www.qrrapido.site")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// Health checks (basic implementation without external dependencies)
builder.Services.AddHealthChecks();
var app = builder.Build();
// Configure the HTTP request pipeline
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("AllowSpecificOrigins");
// Localization middleware
app.UseRequestLocalization();
app.UseAuthentication();
app.UseAuthorization();
// Custom middleware
app.UseMiddleware<LastLoginUpdateMiddleware>();
// Health check endpoint
app.MapHealthChecks("/health");
// Controller routes
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// API routes
app.MapControllerRoute(
name: "api",
pattern: "api/{controller}/{action=Index}/{id?}");
// Language routes
app.MapControllerRoute(
name: "localized",
pattern: "{culture:regex(^(pt-BR|es|en)$)}/{controller=Home}/{action=Index}/{id?}");
app.Run();

View File

@ -0,0 +1,12 @@
{
"profiles": {
"QRRapidoApp": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52428;https://192.168.0.85:52428;http://localhost:52429"
}
}
}

39
QRRapidoApp.csproj Normal file
View File

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>QRRapidoApp</AssemblyName>
<RootNamespace>QRRapidoApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.22.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Stripe.net" Version="43.15.0" />
<PackageReference Include="StackExchange.Redis" Version="2.7.4" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Localization" Version="2.2.0" />
<PackageReference Include="xunit.assert" Version="2.9.3" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\images\" />
<Folder Include="wwwroot\css\" />
<Folder Include="wwwroot\js\" />
<Folder Include="Resources\" />
<Folder Include="Data\" />
<Folder Include="Services\" />
<Folder Include="Models\" />
<Folder Include="Middleware\" />
<Folder Include="Tests\" />
</ItemGroup>
</Project>

25
QRRapidoApp.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35818.85 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRRapidoApp", "QRRapidoApp.csproj", "{8AF92774-40E8-830E-08B3-67F0A0B91DDE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9E53D8E2-0957-4925-B347-404E3B14587B}
EndGlobalSection
EndGlobal

223
Resources/Views.es.resx Normal file
View File

@ -0,0 +1,223 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Tagline" xml:space="preserve">
<value>¡Genera códigos QR en segundos!</value>
</data>
<data name="GenerateQR" xml:space="preserve">
<value>Generar Código QR</value>
</data>
<data name="QRType" xml:space="preserve">
<value>Tipo de Código QR</value>
</data>
<data name="Content" xml:space="preserve">
<value>Contenido</value>
</data>
<data name="URLType" xml:space="preserve">
<value>URL/Enlace</value>
</data>
<data name="TextType" xml:space="preserve">
<value>Texto Simple</value>
</data>
<data name="WiFiType" xml:space="preserve">
<value>WiFi</value>
</data>
<data name="VCardType" xml:space="preserve">
<value>Tarjeta de Visita</value>
</data>
<data name="SMSType" xml:space="preserve">
<value>SMS</value>
</data>
<data name="EmailType" xml:space="preserve">
<value>Email</value>
</data>
<data name="DynamicType" xml:space="preserve">
<value>QR Dinámico (Premium)</value>
</data>
<data name="QuickStyle" xml:space="preserve">
<value>Estilo Rápido</value>
</data>
<data name="ClassicStyle" xml:space="preserve">
<value>Clásico</value>
</data>
<data name="ModernStyle" xml:space="preserve">
<value>Moderno</value>
</data>
<data name="ColorfulStyle" xml:space="preserve">
<value>Colorido</value>
</data>
<data name="ContentPlaceholder" xml:space="preserve">
<value>Escribe el contenido de tu código QR aquí...</value>
</data>
<data name="AdvancedCustomization" xml:space="preserve">
<value>Personalización Avanzada</value>
</data>
<data name="PrimaryColor" xml:space="preserve">
<value>Color Principal</value>
</data>
<data name="BackgroundColor" xml:space="preserve">
<value>Color de Fondo</value>
</data>
<data name="Size" xml:space="preserve">
<value>Tamaño</value>
</data>
<data name="Margin" xml:space="preserve">
<value>Margen</value>
</data>
<data name="Logo" xml:space="preserve">
<value>Logo/Icono</value>
</data>
<data name="CornerStyle" xml:space="preserve">
<value>Estilo de Bordes</value>
</data>
<data name="GenerateRapidly" xml:space="preserve">
<value>Generar Código QR Rápidamente</value>
</data>
<data name="Preview" xml:space="preserve">
<value>Vista Previa</value>
</data>
<data name="PreviewPlaceholder" xml:space="preserve">
<value>Tu código QR aparecerá aquí en segundos</value>
</data>
<data name="UltraFastGeneration" xml:space="preserve">
<value>Generación ultra-rápida garantizada</value>
</data>
<data name="DownloadPNG" xml:space="preserve">
<value>Descargar PNG</value>
</data>
<data name="DownloadSVG" xml:space="preserve">
<value>Descargar SVG (Vectorial)</value>
</data>
<data name="DownloadPDF" xml:space="preserve">
<value>Descargar PDF</value>
</data>
<data name="SaveToHistory" xml:space="preserve">
<value>Guardar en Historial</value>
</data>
<data name="LoginToSave" xml:space="preserve">
<value>Inicia sesión para guardar en el historial</value>
</data>
<data name="PremiumTitle" xml:space="preserve">
<value>QR Rapido Premium</value>
</data>
<data name="SpeedTipsTitle" xml:space="preserve">
<value>Consejos para QR Más Rápidos</value>
</data>
<data name="SpeedTip1" xml:space="preserve">
<value>URLs cortas se generan más rápido</value>
</data>
<data name="SpeedTip2" xml:space="preserve">
<value>Menos texto = mayor velocidad</value>
</data>
<data name="SpeedTip3" xml:space="preserve">
<value>Colores sólidos optimizan el proceso</value>
</data>
<data name="SpeedTip4" xml:space="preserve">
<value>Tamaños menores aceleran la descarga</value>
</data>
<data name="Login" xml:space="preserve">
<value>Iniciar Sesión</value>
</data>
<data name="LoginWith" xml:space="preserve">
<value>Iniciar sesión con</value>
</data>
<data name="Google" xml:space="preserve">
<value>Google</value>
</data>
<data name="Microsoft" xml:space="preserve">
<value>Microsoft</value>
</data>
<data name="AdFreeOffer" xml:space="preserve">
<value>¡Login = 30 días sin anuncios!</value>
</data>
<data name="SpecialOffer" xml:space="preserve">
<value>¡Oferta Especial!</value>
</data>
<data name="LoginBenefits" xml:space="preserve">
<value>Al iniciar sesión, ganas automáticamente 30 días sin anuncios y puedes generar hasta 50 códigos QR por día gratis.</value>
</data>
<data name="Privacy" xml:space="preserve">
<value>Política de Privacidad</value>
</data>
<data name="BackToGenerator" xml:space="preserve">
<value>Volver al generador</value>
</data>
<data name="GeneratedIn" xml:space="preserve">
<value>Generado en</value>
</data>
<data name="Seconds" xml:space="preserve">
<value>s</value>
</data>
<data name="UltraFast" xml:space="preserve">
<value>¡Generación ultra rápida!</value>
</data>
<data name="Fast" xml:space="preserve">
<value>¡Generación rápida!</value>
</data>
<data name="Normal" xml:space="preserve">
<value>Generación normal</value>
</data>
<data name="Error" xml:space="preserve">
<value>Error en la generación. Inténtalo de nuevo.</value>
</data>
<data name="Success" xml:space="preserve">
<value>¡Código QR guardado en el historial!</value>
</data>
</root>

223
Resources/Views.pt-BR.resx Normal file
View File

@ -0,0 +1,223 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Tagline" xml:space="preserve">
<value>Gere QR codes em segundos!</value>
</data>
<data name="GenerateQR" xml:space="preserve">
<value>Gerar QR Code</value>
</data>
<data name="QRType" xml:space="preserve">
<value>Tipo de QR Code</value>
</data>
<data name="Content" xml:space="preserve">
<value>Conteúdo</value>
</data>
<data name="URLType" xml:space="preserve">
<value>URL/Link</value>
</data>
<data name="TextType" xml:space="preserve">
<value>Texto Simples</value>
</data>
<data name="WiFiType" xml:space="preserve">
<value>WiFi</value>
</data>
<data name="VCardType" xml:space="preserve">
<value>Cartão de Visita</value>
</data>
<data name="SMSType" xml:space="preserve">
<value>SMS</value>
</data>
<data name="EmailType" xml:space="preserve">
<value>Email</value>
</data>
<data name="DynamicType" xml:space="preserve">
<value>QR Dinâmico (Premium)</value>
</data>
<data name="QuickStyle" xml:space="preserve">
<value>Estilo Rápido</value>
</data>
<data name="ClassicStyle" xml:space="preserve">
<value>Clássico</value>
</data>
<data name="ModernStyle" xml:space="preserve">
<value>Moderno</value>
</data>
<data name="ColorfulStyle" xml:space="preserve">
<value>Colorido</value>
</data>
<data name="ContentPlaceholder" xml:space="preserve">
<value>Digite o conteúdo do seu QR code aqui...</value>
</data>
<data name="AdvancedCustomization" xml:space="preserve">
<value>Personalização Avançada</value>
</data>
<data name="PrimaryColor" xml:space="preserve">
<value>Cor Principal</value>
</data>
<data name="BackgroundColor" xml:space="preserve">
<value>Cor de Fundo</value>
</data>
<data name="Size" xml:space="preserve">
<value>Tamanho</value>
</data>
<data name="Margin" xml:space="preserve">
<value>Margem</value>
</data>
<data name="Logo" xml:space="preserve">
<value>Logo/Ícone</value>
</data>
<data name="CornerStyle" xml:space="preserve">
<value>Estilo das Bordas</value>
</data>
<data name="GenerateRapidly" xml:space="preserve">
<value>Gerar QR Code Rapidamente</value>
</data>
<data name="Preview" xml:space="preserve">
<value>Preview</value>
</data>
<data name="PreviewPlaceholder" xml:space="preserve">
<value>Seu QR code aparecerá aqui em segundos</value>
</data>
<data name="UltraFastGeneration" xml:space="preserve">
<value>Geração ultra-rápida garantida</value>
</data>
<data name="DownloadPNG" xml:space="preserve">
<value>Download PNG</value>
</data>
<data name="DownloadSVG" xml:space="preserve">
<value>Download SVG (Vetorial)</value>
</data>
<data name="DownloadPDF" xml:space="preserve">
<value>Download PDF</value>
</data>
<data name="SaveToHistory" xml:space="preserve">
<value>Salvar no Histórico</value>
</data>
<data name="LoginToSave" xml:space="preserve">
<value>Faça login para salvar no histórico</value>
</data>
<data name="PremiumTitle" xml:space="preserve">
<value>QR Rapido Premium</value>
</data>
<data name="SpeedTipsTitle" xml:space="preserve">
<value>Dicas para QR Mais Rápidos</value>
</data>
<data name="SpeedTip1" xml:space="preserve">
<value>URLs curtas geram mais rápido</value>
</data>
<data name="SpeedTip2" xml:space="preserve">
<value>Menos texto = maior velocidade</value>
</data>
<data name="SpeedTip3" xml:space="preserve">
<value>Cores sólidas otimizam o processo</value>
</data>
<data name="SpeedTip4" xml:space="preserve">
<value>Tamanhos menores aceleram o download</value>
</data>
<data name="Login" xml:space="preserve">
<value>Login</value>
</data>
<data name="LoginWith" xml:space="preserve">
<value>Entrar com</value>
</data>
<data name="Google" xml:space="preserve">
<value>Google</value>
</data>
<data name="Microsoft" xml:space="preserve">
<value>Microsoft</value>
</data>
<data name="AdFreeOffer" xml:space="preserve">
<value>Login = 30 dias sem anúncios!</value>
</data>
<data name="SpecialOffer" xml:space="preserve">
<value>Oferta Especial!</value>
</data>
<data name="LoginBenefits" xml:space="preserve">
<value>Ao fazer login, você ganha automaticamente 30 dias sem anúncios e pode gerar até 50 QR codes por dia gratuitamente.</value>
</data>
<data name="Privacy" xml:space="preserve">
<value>Política de Privacidade</value>
</data>
<data name="BackToGenerator" xml:space="preserve">
<value>Voltar ao gerador</value>
</data>
<data name="GeneratedIn" xml:space="preserve">
<value>Gerado em</value>
</data>
<data name="Seconds" xml:space="preserve">
<value>s</value>
</data>
<data name="UltraFast" xml:space="preserve">
<value>Geração ultra rápida!</value>
</data>
<data name="Fast" xml:space="preserve">
<value>Geração rápida!</value>
</data>
<data name="Normal" xml:space="preserve">
<value>Geração normal</value>
</data>
<data name="Error" xml:space="preserve">
<value>Erro na geração. Tente novamente.</value>
</data>
<data name="Success" xml:space="preserve">
<value>QR Code salvo no histórico!</value>
</data>
</root>

View File

@ -0,0 +1,117 @@
using MongoDB.Driver;
using QRRapidoApp.Data;
using QRRapidoApp.Models;
namespace QRRapidoApp.Services
{
public class AdDisplayService
{
private readonly IUserService _userService;
private readonly MongoDbContext _context;
private readonly IConfiguration _config;
private readonly ILogger<AdDisplayService> _logger;
public AdDisplayService(IUserService userService, MongoDbContext context, IConfiguration config, ILogger<AdDisplayService> logger)
{
_userService = userService;
_context = context;
_config = config;
_logger = logger;
}
public async Task<bool> ShouldShowAds(string? userId = null)
{
try
{
// Usuários não logados: sempre mostrar anúncios
if (string.IsNullOrEmpty(userId))
return true;
var user = await _userService.GetUserAsync(userId);
if (user == null) return true;
// APENAS Premium users não veem anúncios
return !(user.IsPremium && user.PremiumExpiresAt > DateTime.UtcNow);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error checking ad display status for user {userId}: {ex.Message}");
return true; // Default to showing ads on error
}
}
// MÉTODO REMOVIDO: GetAdFreeTimeRemaining - não é mais necessário
// MÉTODO REMOVIDO: GetActiveAdFreeSessionAsync - não é mais necessário
public async Task<bool> HasValidPremiumSubscription(string userId)
{
try
{
var user = await _userService.GetUserAsync(userId);
return user?.IsPremium == true && user.PremiumExpiresAt > DateTime.UtcNow;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error checking premium subscription for user {userId}: {ex.Message}");
return false;
}
}
public async Task<string> GetAdFreeStatusAsync(string userId)
{
try
{
if (await HasValidPremiumSubscription(userId))
return "Premium";
return "None";
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting ad-free status for user {userId}: {ex.Message}");
return "None";
}
}
public async Task<DateTime?> GetAdFreeExpiryDate(string userId)
{
try
{
var user = await _userService.GetUserAsync(userId);
if (user?.IsPremium == true && user.PremiumExpiresAt > DateTime.UtcNow)
return user.PremiumExpiresAt;
return null; // Sem sessões ad-free temporárias
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting ad-free expiry date for user {userId}: {ex.Message}");
return null;
}
}
public async Task DeactivateExpiredSessionsAsync()
{
try
{
var filter = Builders<AdFreeSession>.Filter.And(
Builders<AdFreeSession>.Filter.Eq(s => s.IsActive, true),
Builders<AdFreeSession>.Filter.Lt(s => s.ExpiresAt, DateTime.UtcNow)
);
var update = Builders<AdFreeSession>.Update.Set(s => s.IsActive, false);
var result = await _context.AdFreeSessions.UpdateManyAsync(filter, update);
if (result.ModifiedCount > 0)
{
_logger.LogInformation($"Deactivated {result.ModifiedCount} expired ad-free sessions");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error deactivating expired sessions: {ex.Message}");
}
}
}
}

View File

@ -0,0 +1,70 @@
using QRRapidoApp.Data;
using QRRapidoApp.Models;
using MongoDB.Driver;
namespace QRRapidoApp.Services
{
public class HistoryCleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<HistoryCleanupService> _logger;
private readonly IConfiguration _configuration;
public HistoryCleanupService(IServiceScopeFactory scopeFactory, ILogger<HistoryCleanupService> logger, IConfiguration configuration)
{
_scopeFactory = scopeFactory;
_logger = logger;
_configuration = configuration;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var gracePeriodDays = _configuration.GetValue<int>("HistoryCleanup:GracePeriodDays", 7);
var cleanupIntervalHours = _configuration.GetValue<int>("HistoryCleanup:CleanupIntervalHours", 6);
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<MongoDbContext>();
if (context.IsConnected && context.Users != null && context.QRCodeHistory != null)
{
// Buscar usuários que cancelaram há mais de X dias
var cutoffDate = DateTime.UtcNow.AddDays(-gracePeriodDays);
var usersToCleanup = await context.Users
.Find(u => u.PremiumCancelledAt != null &&
u.PremiumCancelledAt < cutoffDate &&
u.QRHistoryIds.Count > 0)
.ToListAsync(stoppingToken);
foreach (var user in usersToCleanup)
{
// Remover histórico de QR codes
if (user.QRHistoryIds.Any())
{
await context.QRCodeHistory.DeleteManyAsync(
qr => user.QRHistoryIds.Contains(qr.Id), stoppingToken);
// Limpar lista de histórico do usuário
var update = Builders<User>.Update.Set(u => u.QRHistoryIds, new List<string>());
await context.Users.UpdateOneAsync(u => u.Id == user.Id, update, cancellationToken: stoppingToken);
_logger.LogInformation($"Histórico removido para usuário {user.Id} - inadimplente há {gracePeriodDays}+ dias");
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro no cleanup do histórico");
}
// Executar a cada X horas
await Task.Delay(TimeSpan.FromHours(cleanupIntervalHours), stoppingToken);
}
}
}
}

View File

@ -0,0 +1,13 @@
using QRRapidoApp.Models.ViewModels;
namespace QRRapidoApp.Services
{
public interface IQRCodeService
{
Task<QRGenerationResult> GenerateRapidAsync(QRGenerationRequest request);
Task<byte[]> ConvertToSvgAsync(string qrCodeBase64);
Task<byte[]> ConvertToPdfAsync(string qrCodeBase64, int size = 300);
Task<string> GenerateDynamicQRAsync(QRGenerationRequest request, string userId);
Task<bool> UpdateDynamicQRAsync(string qrId, string newContent);
}
}

26
Services/IUserService.cs Normal file
View File

@ -0,0 +1,26 @@
using QRRapidoApp.Models;
using QRRapidoApp.Models.ViewModels;
namespace QRRapidoApp.Services
{
public interface IUserService
{
Task<User?> GetUserAsync(string userId);
Task<User?> GetUserByEmailAsync(string email);
Task<User?> GetUserByProviderAsync(string provider, string providerId);
Task<User> CreateUserAsync(string email, string name, string provider, string providerId);
Task UpdateLastLoginAsync(string userId);
Task<bool> UpdateUserAsync(User user);
Task<int> GetDailyQRCountAsync(string? userId);
Task<int> DecrementDailyQRCountAsync(string userId);
Task<bool> CanGenerateQRAsync(string? userId, bool isPremium);
Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult);
Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50);
Task<QRCodeHistory?> GetQRDataAsync(string qrId);
Task<int> GetQRCountThisMonthAsync(string userId);
Task<string> GetUserEmailAsync(string userId);
Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt);
Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate);
Task DeleteUserHistoryAsync(string userId);
}
}

View File

@ -0,0 +1,70 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using System.Text;
namespace QRRapidoApp.Services
{
public class MemoryDistributedCacheWrapper : IDistributedCache
{
private readonly IMemoryCache _memoryCache;
public MemoryDistributedCacheWrapper(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public byte[]? Get(string key)
{
var value = _memoryCache.Get<string>(key);
return value != null ? Encoding.UTF8.GetBytes(value) : null;
}
public Task<byte[]?> GetAsync(string key, CancellationToken token = default)
{
return Task.FromResult(Get(key));
}
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
{
var stringValue = Encoding.UTF8.GetString(value);
var memoryCacheOptions = new MemoryCacheEntryOptions();
if (options.AbsoluteExpiration.HasValue)
memoryCacheOptions.AbsoluteExpiration = options.AbsoluteExpiration;
else if (options.AbsoluteExpirationRelativeToNow.HasValue)
memoryCacheOptions.AbsoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow;
if (options.SlidingExpiration.HasValue)
memoryCacheOptions.SlidingExpiration = options.SlidingExpiration;
_memoryCache.Set(key, stringValue, memoryCacheOptions);
}
public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
{
Set(key, value, options);
return Task.CompletedTask;
}
public void Refresh(string key)
{
// Memory cache doesn't need refresh
}
public Task RefreshAsync(string key, CancellationToken token = default)
{
return Task.CompletedTask;
}
public void Remove(string key)
{
_memoryCache.Remove(key);
}
public Task RemoveAsync(string key, CancellationToken token = default)
{
Remove(key);
return Task.CompletedTask;
}
}
}

257
Services/QRRapidoService.cs Normal file
View File

@ -0,0 +1,257 @@
using Microsoft.Extensions.Caching.Distributed;
using QRCoder;
using QRRapidoApp.Models.ViewModels;
using System.Diagnostics;
using System.Drawing;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace QRRapidoApp.Services
{
public class QRRapidoService : IQRCodeService
{
private readonly IDistributedCache _cache;
private readonly IConfiguration _config;
private readonly ILogger<QRRapidoService> _logger;
private readonly SemaphoreSlim _semaphore;
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger)
{
_cache = cache;
_config = config;
_logger = logger;
// Limit simultaneous generations to maintain performance
var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100);
_semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
}
public async Task<QRGenerationResult> GenerateRapidAsync(QRGenerationRequest request)
{
var stopwatch = Stopwatch.StartNew();
try
{
await _semaphore.WaitAsync();
// Cache key based on content and settings
var cacheKey = GenerateCacheKey(request);
var cached = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cached))
{
stopwatch.Stop();
_logger.LogInformation($"QR code served from cache in {stopwatch.ElapsedMilliseconds}ms");
var cachedResult = JsonSerializer.Deserialize<QRGenerationResult>(cached);
if (cachedResult != null)
{
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
cachedResult.FromCache = true;
return cachedResult;
}
}
// Optimized generation
var qrCode = await GenerateQRCodeOptimizedAsync(request);
var base64 = Convert.ToBase64String(qrCode);
var result = new QRGenerationResult
{
QRCodeBase64 = base64,
QRId = Guid.NewGuid().ToString(),
GenerationTimeMs = stopwatch.ElapsedMilliseconds,
FromCache = false,
Size = qrCode.Length,
RequestSettings = request,
Success = true
};
// Cache for configurable time
var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60));
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheExpiration
});
stopwatch.Stop();
_logger.LogInformation($"QR code generated in {stopwatch.ElapsedMilliseconds}ms for type {request.Type}");
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in rapid QR code generation: {ex.Message}");
return new QRGenerationResult
{
Success = false,
ErrorMessage = ex.Message,
GenerationTimeMs = stopwatch.ElapsedMilliseconds
};
}
finally
{
_semaphore.Release();
}
}
private async Task<byte[]> GenerateQRCodeOptimizedAsync(QRGenerationRequest request)
{
return await Task.Run(() =>
{
using var qrGenerator = new QRCodeGenerator();
using var qrCodeData = qrGenerator.CreateQrCode(request.Content, GetErrorCorrectionLevel(request));
// Optimized settings for speed
using var qrCode = new PngByteQRCode(qrCodeData);
// Apply optimizations based on user type
var pixelsPerModule = request.IsPremium ?
GetOptimalPixelsPerModule(request.Size) :
Math.Max(8, request.Size / 40); // Lower quality for free users, but faster
var primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor));
var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor));
return qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes);
});
}
private QRCodeGenerator.ECCLevel GetErrorCorrectionLevel(QRGenerationRequest request)
{
// Lower error correction = faster generation
if (request.OptimizeForSpeed)
{
return QRCodeGenerator.ECCLevel.L; // ~7% correction
}
return request.HasLogo ?
QRCodeGenerator.ECCLevel.H : // ~30% correction for logos
QRCodeGenerator.ECCLevel.M; // ~15% correction default
}
private int GetOptimalPixelsPerModule(int targetSize)
{
// Optimized algorithm for best quality/speed ratio
return targetSize switch
{
<= 200 => 8,
<= 300 => 12,
<= 500 => 16,
<= 800 => 20,
_ => 24
};
}
private string GenerateCacheKey(QRGenerationRequest request)
{
var keyData = $"{request.Content}|{request.Type}|{request.Size}|{request.PrimaryColor}|{request.BackgroundColor}|{request.QuickStyle}|{request.CornerStyle}|{request.Margin}";
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
return $"qr_rapid_{Convert.ToBase64String(hash)[..16]}";
}
public async Task<byte[]> ConvertToSvgAsync(string qrCodeBase64)
{
return await Task.Run(() =>
{
// Convert PNG to SVG (simplified implementation)
var svgContent = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<svg xmlns=""http://www.w3.org/2000/svg"" viewBox=""0 0 300 300"">
<rect width=""300"" height=""300"" fill=""white""/>
<image href=""data:image/png;base64,{qrCodeBase64}"" width=""300"" height=""300""/>
</svg>";
return Encoding.UTF8.GetBytes(svgContent);
});
}
public async Task<byte[]> ConvertToPdfAsync(string qrCodeBase64, int size = 300)
{
return await Task.Run(() =>
{
// Simplified PDF generation - in real implementation, use iTextSharp or similar
var pdfHeader = "%PDF-1.4\n";
var pdfBody = $"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n" +
$"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" +
$"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 {size} {size}]>>endobj\n" +
$"xref\n0 4\n0000000000 65535 f \n0000000010 00000 n \n0000000053 00000 n \n0000000125 00000 n \n";
var pdfContent = pdfBody + $"trailer<</Size 4/Root 1 0 R>>\nstartxref\n{pdfHeader.Length + pdfBody.Length}\n%%EOF";
return Encoding.UTF8.GetBytes(pdfHeader + pdfContent);
});
}
public async Task<string> GenerateDynamicQRAsync(QRGenerationRequest request, string userId)
{
// For premium users only - dynamic QR codes that can be edited
var dynamicId = Guid.NewGuid().ToString();
var dynamicUrl = $"https://qrrapido.site/d/{dynamicId}";
// Store mapping in cache/database
var cacheKey = $"dynamic_qr_{dynamicId}";
await _cache.SetStringAsync(cacheKey, request.Content, new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromDays(365) // Long-lived for premium users
});
return dynamicId;
}
public async Task<bool> UpdateDynamicQRAsync(string qrId, string newContent)
{
try
{
var cacheKey = $"dynamic_qr_{qrId}";
await _cache.SetStringAsync(cacheKey, newContent, new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromDays(365)
});
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to update dynamic QR {qrId}: {ex.Message}");
return false;
}
}
private System.Drawing.Color ParseHtmlColor(string htmlColor)
{
if (string.IsNullOrEmpty(htmlColor) || !htmlColor.StartsWith("#"))
{
return System.Drawing.Color.Black;
}
try
{
if (htmlColor.Length == 7) // #RRGGBB
{
var r = Convert.ToByte(htmlColor.Substring(1, 2), 16);
var g = Convert.ToByte(htmlColor.Substring(3, 2), 16);
var b = Convert.ToByte(htmlColor.Substring(5, 2), 16);
return System.Drawing.Color.FromArgb(r, g, b);
}
else if (htmlColor.Length == 4) // #RGB
{
var r = Convert.ToByte(htmlColor.Substring(1, 1) + htmlColor.Substring(1, 1), 16);
var g = Convert.ToByte(htmlColor.Substring(2, 1) + htmlColor.Substring(2, 1), 16);
var b = Convert.ToByte(htmlColor.Substring(3, 1) + htmlColor.Substring(3, 1), 16);
return System.Drawing.Color.FromArgb(r, g, b);
}
}
catch
{
return System.Drawing.Color.Black;
}
return System.Drawing.Color.Black;
}
private byte[] ColorToBytes(System.Drawing.Color color)
{
return new byte[] { color.R, color.G, color.B };
}
}
}

299
Services/StripeService.cs Normal file
View File

@ -0,0 +1,299 @@
using Stripe;
using Stripe.Checkout;
using QRRapidoApp.Models;
namespace QRRapidoApp.Services
{
public class StripeService
{
private readonly IConfiguration _config;
private readonly IUserService _userService;
private readonly ILogger<StripeService> _logger;
public StripeService(IConfiguration config, IUserService userService, ILogger<StripeService> logger)
{
_config = config;
_userService = userService;
_logger = logger;
}
public async Task<string> CreateCheckoutSessionAsync(string userId, string priceId)
{
try
{
var options = new SessionCreateOptions
{
PaymentMethodTypes = new List<string> { "card" },
Mode = "subscription",
LineItems = new List<SessionLineItemOptions>
{
new SessionLineItemOptions
{
Price = priceId,
Quantity = 1
}
},
ClientReferenceId = userId,
SuccessUrl = $"{_config["App:BaseUrl"]}/Premium/Success?session_id={{CHECKOUT_SESSION_ID}}",
CancelUrl = $"{_config["App:BaseUrl"]}/Premium/Cancel",
CustomerEmail = await _userService.GetUserEmailAsync(userId),
AllowPromotionCodes = true,
BillingAddressCollection = "auto",
Metadata = new Dictionary<string, string>
{
{ "userId", userId },
{ "product", "QR Rapido Premium" }
}
};
var service = new SessionService();
var session = await service.CreateAsync(options);
_logger.LogInformation($"Created Stripe checkout session for user {userId}: {session.Id}");
return session.Url;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error creating Stripe checkout session for user {userId}: {ex.Message}");
throw;
}
}
public async Task HandleWebhookAsync(string json, string signature)
{
var webhookSecret = _config["Stripe:WebhookSecret"];
try
{
var stripeEvent = EventUtility.ConstructEvent(json, signature, webhookSecret);
_logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}");
switch (stripeEvent.Type)
{
case "checkout.session.completed":
var session = stripeEvent.Data.Object as Session;
if (session != null)
{
await ActivatePremiumAsync(session.ClientReferenceId, session.CustomerId, session.SubscriptionId);
}
break;
case "invoice.payment_succeeded":
var invoice = stripeEvent.Data.Object as Invoice;
if (invoice != null && invoice.SubscriptionId != null)
{
await RenewPremiumSubscriptionAsync(invoice.SubscriptionId);
}
break;
case "invoice.payment_failed":
var failedInvoice = stripeEvent.Data.Object as Invoice;
if (failedInvoice != null && failedInvoice.SubscriptionId != null)
{
await HandleFailedPaymentAsync(failedInvoice.SubscriptionId);
}
break;
case "customer.subscription.deleted":
var deletedSubscription = stripeEvent.Data.Object as Subscription;
if (deletedSubscription != null)
{
await DeactivatePremiumAsync(deletedSubscription);
}
break;
case "customer.subscription.updated":
var updatedSubscription = stripeEvent.Data.Object as Subscription;
if (updatedSubscription != null)
{
await UpdateSubscriptionAsync(updatedSubscription);
}
break;
default:
_logger.LogWarning($"Unhandled Stripe webhook event type: {stripeEvent.Type}");
break;
}
}
catch (StripeException ex)
{
_logger.LogError(ex, $"Stripe webhook error: {ex.Message}");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing Stripe webhook: {ex.Message}");
throw;
}
}
private async Task ActivatePremiumAsync(string? userId, string? customerId, string? subscriptionId)
{
if (string.IsNullOrEmpty(userId)) return;
try
{
var user = await _userService.GetUserAsync(userId);
if (user == null)
{
_logger.LogWarning($"User not found for premium activation: {userId}");
return;
}
user.IsPremium = true;
user.StripeCustomerId = customerId;
user.StripeSubscriptionId = subscriptionId;
user.PremiumExpiresAt = DateTime.UtcNow.AddDays(32); // Buffer for billing cycles
await _userService.UpdateUserAsync(user);
_logger.LogInformation($"Activated premium for user {userId}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error activating premium for user {userId}: {ex.Message}");
}
}
private async Task RenewPremiumSubscriptionAsync(string subscriptionId)
{
try
{
// Find user by subscription ID
var user = await FindUserBySubscriptionIdAsync(subscriptionId);
if (user == null) return;
// Extend premium expiry
user.PremiumExpiresAt = DateTime.UtcNow.AddDays(32);
await _userService.UpdateUserAsync(user);
_logger.LogInformation($"Renewed premium subscription for user {user.Id}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error renewing premium subscription {subscriptionId}: {ex.Message}");
}
}
private async Task HandleFailedPaymentAsync(string subscriptionId)
{
try
{
var user = await FindUserBySubscriptionIdAsync(subscriptionId);
if (user == null) return;
// Don't immediately deactivate - Stripe will retry
_logger.LogWarning($"Payment failed for user {user.Id}, subscription {subscriptionId}");
// Could send notification email here
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error handling failed payment for subscription {subscriptionId}: {ex.Message}");
}
}
private async Task DeactivatePremiumAsync(Subscription subscription)
{
try
{
var user = await FindUserBySubscriptionIdAsync(subscription.Id);
if (user == null) return;
// ADICIONAR: marcar data de cancelamento
await _userService.MarkPremiumCancelledAsync(user.Id, DateTime.UtcNow);
_logger.LogInformation($"Deactivated premium for user {user.Id}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error deactivating premium for subscription {subscription.Id}: {ex.Message}");
}
}
private async Task UpdateSubscriptionAsync(Subscription subscription)
{
try
{
var user = await FindUserBySubscriptionIdAsync(subscription.Id);
if (user == null) return;
// Update based on subscription status
if (subscription.Status == "active")
{
user.IsPremium = true;
user.PremiumExpiresAt = subscription.CurrentPeriodEnd.AddDays(2); // Small buffer
}
else if (subscription.Status == "canceled" || subscription.Status == "unpaid")
{
user.IsPremium = false;
user.PremiumExpiresAt = DateTime.UtcNow;
}
await _userService.UpdateUserAsync(user);
_logger.LogInformation($"Updated subscription for user {user.Id}: {subscription.Status}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating subscription {subscription.Id}: {ex.Message}");
}
}
private async Task<User?> FindUserBySubscriptionIdAsync(string subscriptionId)
{
try
{
// This would require implementing a method in UserService to find by subscription ID
// For now, we'll leave this as a placeholder
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error finding user by subscription ID {subscriptionId}: {ex.Message}");
return null;
}
}
public async Task<string> GetSubscriptionStatusAsync(string? subscriptionId)
{
if (string.IsNullOrEmpty(subscriptionId))
return "None";
try
{
var service = new SubscriptionService();
var subscription = await service.GetAsync(subscriptionId);
return subscription.Status;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting subscription status for {subscriptionId}: {ex.Message}");
return "Unknown";
}
}
public async Task<bool> CancelSubscriptionAsync(string subscriptionId)
{
try
{
var service = new SubscriptionService();
var subscription = await service.CancelAsync(subscriptionId, new SubscriptionCancelOptions
{
InvoiceNow = false,
Prorate = false
});
_logger.LogInformation($"Canceled subscription {subscriptionId}");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error canceling subscription {subscriptionId}: {ex.Message}");
return false;
}
}
}
}

382
Services/UserService.cs Normal file
View File

@ -0,0 +1,382 @@
using MongoDB.Driver;
using QRRapidoApp.Data;
using QRRapidoApp.Models;
using QRRapidoApp.Models.ViewModels;
using System.Text.Json;
namespace QRRapidoApp.Services
{
public class UserService : IUserService
{
private readonly MongoDbContext _context;
private readonly IConfiguration _config;
private readonly ILogger<UserService> _logger;
public UserService(MongoDbContext context, IConfiguration config, ILogger<UserService> logger)
{
_context = context;
_config = config;
_logger = logger;
}
public async Task<User?> GetUserAsync(string userId)
{
try
{
if (_context.Users == null) return null; // Development mode without MongoDB
return await _context.Users.Find(u => u.Id == userId).FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting user {userId}: {ex.Message}");
return null;
}
}
public async Task<User?> GetUserByEmailAsync(string email)
{
try
{
if (_context.Users == null) return null; // Development mode without MongoDB
return await _context.Users.Find(u => u.Email == email).FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting user by email {email}: {ex.Message}");
return null;
}
}
public async Task<User?> GetUserByProviderAsync(string provider, string providerId)
{
try
{
return await _context.Users
.Find(u => u.Provider == provider && u.ProviderId == providerId)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting user by provider {provider}:{providerId}: {ex.Message}");
return null;
}
}
public async Task<User> CreateUserAsync(string email, string name, string provider, string providerId)
{
var user = new User
{
Email = email,
Name = name,
Provider = provider,
ProviderId = providerId,
CreatedAt = DateTime.UtcNow,
LastLoginAt = DateTime.UtcNow,
PreferredLanguage = "pt-BR",
DailyQRCount = 0,
LastQRDate = DateTime.UtcNow.Date,
TotalQRGenerated = 0
};
await _context.Users.InsertOneAsync(user);
_logger.LogInformation($"Created new user: {email} via {provider}");
// Create initial ad-free session for new users
await CreateAdFreeSessionAsync(user.Id, "Login");
return user;
}
public async Task UpdateLastLoginAsync(string userId)
{
try
{
var update = Builders<User>.Update
.Set(u => u.LastLoginAt, DateTime.UtcNow);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
// Create new ad-free session if needed
await CreateAdFreeSessionAsync(userId, "Login");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating last login for user {userId}: {ex.Message}");
}
}
public async Task<bool> UpdateUserAsync(User user)
{
try
{
var result = await _context.Users.ReplaceOneAsync(u => u.Id == user.Id, user);
return result.ModifiedCount > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating user {user.Id}: {ex.Message}");
return false;
}
}
public async Task<int> GetDailyQRCountAsync(string? userId)
{
if (string.IsNullOrEmpty(userId))
return 0; // Anonymous users tracked separately
try
{
var user = await GetUserAsync(userId);
if (user == null) return 0;
// Reset count if it's a new day
if (user.LastQRDate.Date < DateTime.UtcNow.Date)
{
user.DailyQRCount = 0;
user.LastQRDate = DateTime.UtcNow.Date;
await UpdateUserAsync(user);
}
return user.DailyQRCount;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting daily QR count for user {userId}: {ex.Message}");
return 0;
}
}
public async Task<int> DecrementDailyQRCountAsync(string userId)
{
try
{
var user = await GetUserAsync(userId);
if (user == null) return 0;
// Reset count if it's a new day
if (user.LastQRDate.Date < DateTime.UtcNow.Date)
{
user.DailyQRCount = 1;
user.LastQRDate = DateTime.UtcNow.Date;
}
else
{
user.DailyQRCount++;
}
user.TotalQRGenerated++;
await UpdateUserAsync(user);
// Calculate remaining QRs for free users
var dailyLimit = user.IsPremium ? int.MaxValue : _config.GetValue<int>("Premium:FreeQRLimit", 50);
return Math.Max(0, dailyLimit - user.DailyQRCount);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error decrementing daily QR count for user {userId}: {ex.Message}");
return 0;
}
}
public async Task<bool> CanGenerateQRAsync(string? userId, bool isPremium)
{
if (isPremium) return true;
var dailyCount = await GetDailyQRCountAsync(userId);
var limit = string.IsNullOrEmpty(userId) ? 10 : _config.GetValue<int>("Premium:FreeQRLimit", 50);
return dailyCount < limit;
}
public async Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult)
{
try
{
var qrHistory = new QRCodeHistory
{
UserId = userId,
Type = qrResult.RequestSettings?.Type ?? "unknown",
Content = qrResult.RequestSettings?.Content ?? "",
QRCodeBase64 = qrResult.QRCodeBase64,
CustomizationSettings = JsonSerializer.Serialize(qrResult.RequestSettings),
CreatedAt = DateTime.UtcNow,
Language = qrResult.RequestSettings?.Language ?? "pt-BR",
Size = qrResult.RequestSettings?.Size ?? 300,
GenerationTimeMs = qrResult.GenerationTimeMs,
FromCache = qrResult.FromCache,
IsActive = true,
LastAccessedAt = DateTime.UtcNow
};
await _context.QRCodeHistory.InsertOneAsync(qrHistory);
// Update user's QR history IDs if logged in
if (!string.IsNullOrEmpty(userId))
{
var update = Builders<User>.Update
.Push(u => u.QRHistoryIds, qrHistory.Id);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving QR to history: {ex.Message}");
}
}
public async Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50)
{
try
{
return await _context.QRCodeHistory
.Find(q => q.UserId == userId && q.IsActive)
.SortByDescending(q => q.CreatedAt)
.Limit(limit)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting QR history for user {userId}: {ex.Message}");
return new List<QRCodeHistory>();
}
}
public async Task<QRCodeHistory?> GetQRDataAsync(string qrId)
{
try
{
return await _context.QRCodeHistory
.Find(q => q.Id == qrId && q.IsActive)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting QR data {qrId}: {ex.Message}");
return null;
}
}
public async Task<int> GetQRCountThisMonthAsync(string userId)
{
try
{
var startOfMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1);
var endOfMonth = startOfMonth.AddMonths(1);
var count = await _context.QRCodeHistory
.CountDocumentsAsync(q => q.UserId == userId &&
q.CreatedAt >= startOfMonth &&
q.CreatedAt < endOfMonth);
return (int)count;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting monthly QR count for user {userId}: {ex.Message}");
return 0;
}
}
// MÉTODO REMOVIDO: ExtendAdFreeTimeAsync - não é mais necessário
public async Task<string> GetUserEmailAsync(string userId)
{
var user = await GetUserAsync(userId);
return user?.Email ?? string.Empty;
}
public async Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt)
{
try
{
if (_context.Users == null) return; // Development mode without MongoDB
var update = Builders<User>.Update
.Set(u => u.IsPremium, false)
.Set(u => u.PremiumCancelledAt, cancelledAt)
.Set(u => u.PremiumExpiresAt, null);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
_logger.LogInformation($"Marked premium as cancelled for user {userId} at {cancelledAt}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error marking premium cancelled for user {userId}: {ex.Message}");
}
}
public async Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate)
{
try
{
if (_context.Users == null) return new List<User>(); // Development mode without MongoDB
return await _context.Users
.Find(u => u.PremiumCancelledAt != null &&
u.PremiumCancelledAt < cutoffDate &&
u.QRHistoryIds.Count > 0)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting users for history cleanup: {ex.Message}");
return new List<User>();
}
}
public async Task DeleteUserHistoryAsync(string userId)
{
try
{
if (_context.Users == null || _context.QRCodeHistory == null) return; // Development mode without MongoDB
var user = await GetUserAsync(userId);
if (user?.QRHistoryIds?.Any() == true)
{
// Remover histórico de QR codes
await _context.QRCodeHistory.DeleteManyAsync(qr => user.QRHistoryIds.Contains(qr.Id));
// Limpar lista de histórico do usuário
var update = Builders<User>.Update.Set(u => u.QRHistoryIds, new List<string>());
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
_logger.LogInformation($"Deleted history for user {userId}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error deleting history for user {userId}: {ex.Message}");
}
}
private async Task CreateAdFreeSessionAsync(string userId, string sessionType, int? customMinutes = null)
{
try
{
var durationMinutes = customMinutes ?? _config.GetValue<int>("AdFree:LoginMinutes", 43200);
var session = new AdFreeSession
{
UserId = userId,
StartedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddMinutes(durationMinutes),
IsActive = true,
SessionType = sessionType,
DurationMinutes = durationMinutes,
CreatedAt = DateTime.UtcNow
};
await _context.AdFreeSessions.InsertOneAsync(session);
_logger.LogInformation($"Created {sessionType} ad-free session for user {userId} - {durationMinutes} minutes");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error creating ad-free session for user {userId}: {ex.Message}");
}
}
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.22.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../QRRapidoApp.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,266 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Moq;
using QRRapidoApp.Data;
using QRRapidoApp.Models;
using QRRapidoApp.Services;
using Xunit;
namespace QRRapidoApp.Tests.Services
{
public class AdDisplayServiceTests
{
private readonly Mock<IUserService> _userServiceMock;
private readonly Mock<MongoDbContext> _contextMock;
private readonly Mock<IConfiguration> _configMock;
private readonly Mock<ILogger<AdDisplayService>> _loggerMock;
private readonly Mock<IMongoCollection<AdFreeSession>> _sessionCollectionMock;
private readonly AdDisplayService _service;
public AdDisplayServiceTests()
{
_userServiceMock = new Mock<IUserService>();
_contextMock = new Mock<MongoDbContext>(Mock.Of<IMongoClient>(), Mock.Of<IConfiguration>());
_configMock = new Mock<IConfiguration>();
_loggerMock = new Mock<ILogger<AdDisplayService>>();
_sessionCollectionMock = new Mock<IMongoCollection<AdFreeSession>>();
_contextMock.Setup(c => c.AdFreeSessions).Returns(_sessionCollectionMock.Object);
_service = new AdDisplayService(
_userServiceMock.Object,
_contextMock.Object,
_configMock.Object,
_loggerMock.Object
);
}
[Fact]
public async Task ShouldShowAds_WithAnonymousUser_ShouldReturnTrue()
{
// Arrange
string? userId = null;
// Act
var result = await _service.ShouldShowAds(userId);
// Assert
Assert.True(result);
}
[Fact]
public async Task ShouldShowAds_WithPremiumUser_ShouldReturnFalse()
{
// Arrange
var userId = "test-user-id";
var user = new User
{
Id = userId,
IsPremium = true,
PremiumExpiresAt = DateTime.UtcNow.AddDays(30)
};
_userServiceMock.Setup(s => s.GetUserAsync(userId))
.ReturnsAsync(user);
// Act
var result = await _service.ShouldShowAds(userId);
// Assert
Assert.False(result);
}
[Fact]
public async Task ShouldShowAds_WithExpiredPremiumUser_ShouldReturnTrue()
{
// Arrange
var userId = "test-user-id";
var user = new User
{
Id = userId,
IsPremium = true,
PremiumExpiresAt = DateTime.UtcNow.AddDays(-1) // Expired
};
_userServiceMock.Setup(s => s.GetUserAsync(userId))
.ReturnsAsync(user);
var cursor = new Mock<IAsyncCursor<AdFreeSession>>();
cursor.Setup(_ => _.Current).Returns(new List<AdFreeSession>());
cursor.SetupSequence(_ => _.MoveNext(It.IsAny<CancellationToken>()))
.Returns(false);
cursor.SetupSequence(_ => _.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
_sessionCollectionMock.Setup(c => c.FindAsync(
It.IsAny<FilterDefinition<AdFreeSession>>(),
It.IsAny<FindOptions<AdFreeSession>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(cursor.Object);
// Act
var result = await _service.ShouldShowAds(userId);
// Assert
Assert.True(result);
}
[Fact]
public async Task ShouldShowAds_WithActiveAdFreeSession_ShouldReturnFalse()
{
// Arrange
var userId = "test-user-id";
var user = new User
{
Id = userId,
IsPremium = false
};
var activeSession = new AdFreeSession
{
UserId = userId,
IsActive = true,
ExpiresAt = DateTime.UtcNow.AddDays(1)
};
_userServiceMock.Setup(s => s.GetUserAsync(userId))
.ReturnsAsync(user);
var cursor = new Mock<IAsyncCursor<AdFreeSession>>();
cursor.Setup(_ => _.Current).Returns(new List<AdFreeSession> { activeSession });
cursor.SetupSequence(_ => _.MoveNext(It.IsAny<CancellationToken>()))
.Returns(true)
.Returns(false);
cursor.SetupSequence(_ => _.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true)
.ReturnsAsync(false);
_sessionCollectionMock.Setup(c => c.FindAsync(
It.IsAny<FilterDefinition<AdFreeSession>>(),
It.IsAny<FindOptions<AdFreeSession>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(cursor.Object);
// Act
var result = await _service.ShouldShowAds(userId);
// Assert
Assert.False(result);
}
// TESTE REMOVIDO: GetAdFreeTimeRemaining_WithPremiumUser_ShouldReturnMaxValue - método não existe mais
// TESTES REMOVIDOS: GetAdFreeTimeRemaining - método não existe mais
[Fact]
public async Task HasValidPremiumSubscription_WithValidPremium_ShouldReturnTrue()
{
// Arrange
var userId = "test-user-id";
var user = new User
{
Id = userId,
IsPremium = true,
PremiumExpiresAt = DateTime.UtcNow.AddDays(30)
};
_userServiceMock.Setup(s => s.GetUserAsync(userId))
.ReturnsAsync(user);
// Act
var result = await _service.HasValidPremiumSubscription(userId);
// Assert
Assert.True(result);
}
[Fact]
public async Task HasValidPremiumSubscription_WithExpiredPremium_ShouldReturnFalse()
{
// Arrange
var userId = "test-user-id";
var user = new User
{
Id = userId,
IsPremium = true,
PremiumExpiresAt = DateTime.UtcNow.AddDays(-1) // Expired
};
_userServiceMock.Setup(s => s.GetUserAsync(userId))
.ReturnsAsync(user);
// Act
var result = await _service.HasValidPremiumSubscription(userId);
// Assert
Assert.False(result);
}
[Fact]
public async Task GetAdFreeStatusAsync_WithPremiumUser_ShouldReturnPremium()
{
// Arrange
var userId = "test-user-id";
var user = new User
{
Id = userId,
IsPremium = true,
PremiumExpiresAt = DateTime.UtcNow.AddDays(30)
};
_userServiceMock.Setup(s => s.GetUserAsync(userId))
.ReturnsAsync(user);
// Act
var result = await _service.GetAdFreeStatusAsync(userId);
// Assert
Assert.Equal("Premium", result);
}
[Fact]
public async Task GetAdFreeStatusAsync_WithActiveSession_ShouldReturnSessionType()
{
// Arrange
var userId = "test-user-id";
var user = new User
{
Id = userId,
IsPremium = false
};
var activeSession = new AdFreeSession
{
UserId = userId,
IsActive = true,
ExpiresAt = DateTime.UtcNow.AddDays(1),
SessionType = "Login"
};
_userServiceMock.Setup(s => s.GetUserAsync(userId))
.ReturnsAsync(user);
var cursor = new Mock<IAsyncCursor<AdFreeSession>>();
cursor.Setup(_ => _.Current).Returns(new List<AdFreeSession> { activeSession });
cursor.SetupSequence(_ => _.MoveNext(It.IsAny<CancellationToken>()))
.Returns(true)
.Returns(false);
cursor.SetupSequence(_ => _.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true)
.ReturnsAsync(false);
_sessionCollectionMock.Setup(c => c.FindAsync(
It.IsAny<FilterDefinition<AdFreeSession>>(),
It.IsAny<FindOptions<AdFreeSession>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(cursor.Object);
// Act
var result = await _service.GetAdFreeStatusAsync(userId);
// Assert
Assert.Equal("Login", result);
}
}
}

View File

@ -0,0 +1,235 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Moq;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
using System.Text;
using Xunit;
namespace QRRapidoApp.Tests.Services
{
public class QRRapidoServiceTests
{
private readonly Mock<IDistributedCache> _cacheMock;
private readonly Mock<IConfiguration> _configMock;
private readonly Mock<ILogger<QRRapidoService>> _loggerMock;
private readonly QRRapidoService _service;
public QRRapidoServiceTests()
{
_cacheMock = new Mock<IDistributedCache>();
_configMock = new Mock<IConfiguration>();
_loggerMock = new Mock<ILogger<QRRapidoService>>();
SetupDefaultConfiguration();
_service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object);
}
private void SetupDefaultConfiguration()
{
_configMock.Setup(c => c.GetValue<int>("Performance:MaxConcurrentGenerations", 100))
.Returns(100);
_configMock.Setup(c => c.GetValue<int>("Performance:CacheExpirationMinutes", 60))
.Returns(60);
}
[Fact]
public async Task GenerateRapidAsync_WithValidRequest_ShouldReturnSuccessResult()
{
// Arrange
var request = new QRGenerationRequest
{
Type = "url",
Content = "https://example.com",
Size = 300,
PrimaryColor = "#000000",
BackgroundColor = "#FFFFFF",
OptimizeForSpeed = true
};
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
.ReturnsAsync((string)null);
// Act
var result = await _service.GenerateRapidAsync(request);
// Assert
Assert.NotNull(result);
Assert.True(result.Success);
Assert.False(string.IsNullOrEmpty(result.QRCodeBase64));
Assert.False(string.IsNullOrEmpty(result.QRId));
Assert.True(result.GenerationTimeMs > 0);
}
[Fact]
public async Task GenerateRapidAsync_WithEmptyContent_ShouldReturnError()
{
// Arrange
var request = new QRGenerationRequest
{
Type = "url",
Content = "",
Size = 300
};
// Act
var result = await _service.GenerateRapidAsync(request);
// Assert
Assert.NotNull(result);
Assert.False(result.Success);
Assert.False(string.IsNullOrEmpty(result.ErrorMessage));
}
[Fact]
public async Task GenerateRapidAsync_WithCachedResult_ShouldReturnFromCache()
{
// Arrange
var request = new QRGenerationRequest
{
Type = "url",
Content = "https://example.com",
Size = 300
};
var cachedResult = new QRGenerationResult
{
QRCodeBase64 = "cached-base64",
QRId = "cached-id",
Success = true,
FromCache = true
};
var cachedJson = System.Text.Json.JsonSerializer.Serialize(cachedResult);
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
.ReturnsAsync(cachedJson);
// Act
var result = await _service.GenerateRapidAsync(request);
// Assert
Assert.NotNull(result);
Assert.True(result.Success);
Assert.True(result.FromCache);
Assert.Equal("cached-base64", result.QRCodeBase64);
}
[Fact]
public async Task ConvertToSvgAsync_WithValidBase64_ShouldReturnSvgContent()
{
// Arrange
var base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
// Act
var result = await _service.ConvertToSvgAsync(base64);
// Assert
Assert.NotNull(result);
var svgString = Encoding.UTF8.GetString(result);
Assert.Contains("<?xml", svgString);
Assert.Contains("<svg", svgString);
Assert.Contains("</svg>", svgString);
}
[Fact]
public async Task ConvertToPdfAsync_WithValidBase64_ShouldReturnPdfContent()
{
// Arrange
var base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
// Act
var result = await _service.ConvertToPdfAsync(base64, 300);
// Assert
Assert.NotNull(result);
var pdfString = Encoding.UTF8.GetString(result);
Assert.Contains("%PDF", pdfString);
}
[Theory]
[InlineData("url", "https://example.com")]
[InlineData("text", "Hello World")]
[InlineData("email", "test@example.com")]
public async Task GenerateRapidAsync_WithDifferentTypes_ShouldHandleAllTypes(string type, string content)
{
// Arrange
var request = new QRGenerationRequest
{
Type = type,
Content = content,
Size = 300,
OptimizeForSpeed = true
};
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
.ReturnsAsync((string)null);
// Act
var result = await _service.GenerateRapidAsync(request);
// Assert
Assert.NotNull(result);
Assert.True(result.Success);
Assert.False(string.IsNullOrEmpty(result.QRCodeBase64));
}
[Fact]
public async Task GenerateRapidAsync_WithPremiumUser_ShouldUseHigherQuality()
{
// Arrange
var request = new QRGenerationRequest
{
Type = "url",
Content = "https://example.com",
Size = 500,
IsPremium = true,
OptimizeForSpeed = true
};
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
.ReturnsAsync((string)null);
// Act
var result = await _service.GenerateRapidAsync(request);
// Assert
Assert.NotNull(result);
Assert.True(result.Success);
Assert.True(result.Size > 0);
}
[Fact]
public async Task GenerateDynamicQRAsync_WithValidData_ShouldReturnDynamicId()
{
// Arrange
var request = new QRGenerationRequest
{
Type = "url",
Content = "https://example.com"
};
var userId = "test-user-id";
// Act
var result = await _service.GenerateDynamicQRAsync(request, userId);
// Assert
Assert.False(string.IsNullOrEmpty(result));
Assert.True(Guid.TryParse(result, out _));
}
[Fact]
public async Task UpdateDynamicQRAsync_WithValidData_ShouldReturnTrue()
{
// Arrange
var qrId = Guid.NewGuid().ToString();
var newContent = "https://updated-example.com";
// Act
var result = await _service.UpdateDynamicQRAsync(qrId, newContent);
// Assert
Assert.True(result);
}
}
}

View File

@ -0,0 +1,92 @@
@{
ViewData["Title"] = "Login";
var returnUrl = ViewBag.ReturnUrl ?? "/";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white text-center">
<h4 class="mb-0">
<i class="fas fa-sign-in-alt"></i> Entrar
</h4>
</div>
<div class="card-body">
<div class="text-center mb-4">
<p class="text-muted">Entre com sua conta e ganhe:</p>
<div class="row text-center">
<div class="col-12 mb-2">
<div class="badge bg-success p-2 w-100">
<i class="fas fa-crown"></i> 30 dias sem anúncios
</div>
</div>
<div class="col-12 mb-2">
<div class="badge bg-primary p-2 w-100">
<i class="fas fa-infinity"></i> 50 QR codes/dia
</div>
</div>
<div class="col-12 mb-2">
<div class="badge bg-info p-2 w-100">
<i class="fas fa-history"></i> Histórico de QR codes
</div>
</div>
</div>
</div>
<div class="d-grid gap-3">
<a href="/Account/LoginGoogle?returnUrl=@returnUrl" class="btn btn-danger btn-lg">
<i class="fab fa-google"></i> Entrar com Google
</a>
<a href="/Account/LoginMicrosoft?returnUrl=@returnUrl" class="btn btn-primary btn-lg">
<i class="fab fa-microsoft"></i> Entrar com Microsoft
</a>
</div>
<hr class="my-4">
<div class="text-center">
<h6 class="text-success">
<i class="fas fa-gift"></i> Oferta Especial!
</h6>
<p class="small text-muted">
Ao fazer login, você ganha automaticamente <strong>30 dias sem anúncios</strong>
e pode gerar até <strong>50 QR codes por dia</strong> gratuitamente.
</p>
</div>
<div class="text-center mt-3">
<small class="text-muted">
Não cadastramos você sem sua permissão. <br>
<a href="/Home/Privacy" class="text-primary">Política de Privacidade</a>
</small>
</div>
</div>
</div>
<div class="text-center mt-3">
<a href="/" class="text-muted">
<i class="fas fa-arrow-left"></i> Voltar ao gerador
</a>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
// Track login attempts
document.querySelectorAll('[href*="Login"]').forEach(link => {
link.addEventListener('click', () => {
if (typeof gtag !== 'undefined') {
gtag('event', 'login_attempt', {
'event_category': 'Authentication',
'method': link.textContent.includes('Google') ? 'google' : 'microsoft'
});
}
});
});
</script>
}

435
Views/Home/Index.cshtml Normal file
View File

@ -0,0 +1,435 @@
@using QRRapidoApp.Services
@inject AdDisplayService AdService
@{
ViewData["Title"] = "Home";
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="container">
<div class="row">
<!-- QR Generator Form -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="h5 mb-0">
<i class="fas fa-qrcode"></i> Criar QR Code Rapidamente
</h3>
</div>
<div class="card-body">
@if (User.Identity.IsAuthenticated)
{
var isPremium = await AdService.HasValidPremiumSubscription(userId);
@if (isPremium)
{
<div class="alert alert-success border-0">
<i class="fas fa-crown text-warning"></i>
<strong>Usuário Premium ativo!</strong>
<span class="badge bg-success">Sem anúncios + Histórico + QR ilimitados</span>
</div>
}
}
<form id="qr-speed-form" class="needs-validation" novalidate>
<!-- Generation timer -->
<div class="row mb-3">
<div class="col-md-8">
<div class="d-flex align-items-center gap-3">
<div class="generation-timer d-none">
<i class="fas fa-stopwatch text-primary"></i>
<span class="fw-bold text-primary">0.0s</span>
</div>
<div class="speed-badge d-none">
<span class="badge bg-success">
<i class="fas fa-bolt"></i> Geração ultra rápida!
</span>
</div>
</div>
</div>
<div class="col-md-4 text-end">
@if (User.Identity.IsAuthenticated)
{
<small class="text-muted">
<span class="qr-counter">Ilimitado hoje</span>
</small>
}
else
{
<small class="text-muted">
<span class="qr-counter">10 QR codes restantes</span>
</small>
}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">
<i class="fas fa-list"></i> Tipo de QR Code
</label>
<select id="qr-type" class="form-select" required>
<option value="">Selecione o tipo...</option>
<option value="url">🌐 URL/Link</option>
<option value="text">📝 Texto Simples</option>
<option value="wifi">📶 WiFi</option>
<option value="vcard">👤 Cartão de Visita</option>
<option value="sms">💬 SMS</option>
<option value="email">📧 Email</option>
@if (User.Identity.IsAuthenticated)
{
<option value="dynamic">⚡ QR Dinâmico (Premium)</option>
}
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">
<i class="fas fa-palette"></i> Estilo Rápido
</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="quick-style" id="style-classic" value="classic" checked>
<label class="btn btn-outline-secondary" for="style-classic">Clássico</label>
<input type="radio" class="btn-check" name="quick-style" id="style-modern" value="modern">
<label class="btn btn-outline-secondary" for="style-modern">Moderno</label>
<input type="radio" class="btn-check" name="quick-style" id="style-colorful" value="colorful">
<label class="btn btn-outline-secondary" for="style-colorful">Colorido</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">
<i class="fas fa-edit"></i> Conteúdo
</label>
<textarea id="qr-content"
class="form-control form-control-lg"
rows="3"
placeholder="Digite o conteúdo do seu QR code aqui..."
required></textarea>
<div class="form-text">
<span id="content-hints">Dicas aparecerão aqui baseadas no tipo selecionado</span>
</div>
</div>
<!-- Advanced customization (collapsible) -->
<div class="accordion mb-3" id="customization-accordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#customization-panel">
<i class="fas fa-sliders-h me-2"></i> Personalização Avançada
</button>
</h2>
<div id="customization-panel" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="row">
<div class="col-md-3 mb-3">
<label class="form-label">Cor Principal</label>
<input type="color" id="primary-color" class="form-control form-control-color" value="#007BFF">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Cor de Fundo</label>
<input type="color" id="bg-color" class="form-control form-control-color" value="#FFFFFF">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Tamanho</label>
<select id="qr-size" class="form-select">
<option value="200">Pequeno (200px)</option>
<option value="300" selected>Médio (300px)</option>
<option value="500">Grande (500px)</option>
<option value="800">XL (800px)</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Margem</label>
<select id="qr-margin" class="form-select">
<option value="1">Mínima</option>
<option value="2" selected>Normal</option>
<option value="4">Grande</option>
</select>
</div>
</div>
@if (User.Identity.IsAuthenticated)
{
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Logo/Ícone</label>
<input type="file" id="logo-upload" class="form-control" accept="image/*">
<div class="form-text">PNG, JPG até 2MB</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Estilo das Bordas</label>
<select id="corner-style" class="form-select">
<option value="square">Quadrado</option>
<option value="rounded">Arredondado</option>
<option value="circle">Circular</option>
<option value="leaf">Folha</option>
</select>
</div>
</div>
}
</div>
</div>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="generate-btn">
<i class="fas fa-bolt"></i> Gerar QR Code Rapidamente
<div class="spinner-border spinner-border-sm ms-2 d-none" role="status">
<span class="visually-hidden">Gerando...</span>
</div>
</button>
</div>
</form>
</div>
</div>
<!-- Speed statistics -->
<div class="row mt-4">
<div class="col-md-4">
<div class="card text-center border-success">
<div class="card-body">
<h5 class="text-success">
<i class="fas fa-stopwatch"></i> 1.2s
</h5>
<small class="text-muted">Tempo médio</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center border-primary">
<div class="card-body">
<h5 class="text-primary">
<i class="fas fa-chart-line"></i> 99.9%
</h5>
<small class="text-muted">Disponibilidade</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center border-warning">
<div class="card-body">
<h5 class="text-warning">
<i class="fas fa-users"></i> <span id="total-qrs">10.5K</span>
</h5>
<small class="text-muted">QRs gerados hoje</small>
</div>
</div>
</div>
</div>
<!-- Ad Space Between Content (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "content" })
</div>
<!-- Sidebar with preview and ads -->
<div class="col-lg-4">
<!-- Preview with timer -->
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-eye"></i> Preview
</h5>
<div class="generation-stats d-none">
<small class="text-success">
<i class="fas fa-check-circle"></i> Gerado em <span class="generation-time">0s</span>
</small>
</div>
</div>
<div class="card-body text-center">
<div id="qr-preview" class="mb-3">
<div class="placeholder-qr p-5">
<i class="fas fa-qrcode fa-4x text-muted mb-3"></i>
<p class="text-muted">Seu QR code aparecerá aqui em segundos</p>
<small class="text-muted">
<i class="fas fa-bolt"></i> Geração ultra-rápida garantida
</small>
</div>
</div>
<div id="download-section" style="display: none;">
<div class="btn-group-vertical w-100 mb-3">
<button id="download-png" class="btn btn-success">
<i class="fas fa-download"></i> Download PNG
</button>
<button id="download-svg" class="btn btn-outline-success">
<i class="fas fa-vector-square"></i> Download SVG (Vetorial)
</button>
<button id="download-pdf" class="btn btn-outline-success">
<i class="fas fa-file-pdf"></i> Download PDF
</button>
</div>
<!-- Share Button with Dropdown -->
<div class="dropdown w-100 mb-3">
<button class="btn btn-primary dropdown-toggle w-100" type="button" id="share-qr-btn" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-share-alt"></i> Compartilhar QR Code
</button>
<ul class="dropdown-menu w-100" aria-labelledby="share-qr-btn" id="share-dropdown">
<!-- Native share option (mobile only) -->
<li class="d-none" id="native-share-option">
<a class="dropdown-item" href="#" id="native-share">
<i class="fas fa-mobile-alt text-primary"></i> Compartilhar (Sistema)
</a>
</li>
<!-- WhatsApp -->
<li>
<a class="dropdown-item" href="#" id="share-whatsapp">
<i class="fab fa-whatsapp text-success"></i> WhatsApp
</a>
</li>
<!-- Telegram -->
<li>
<a class="dropdown-item" href="#" id="share-telegram">
<i class="fab fa-telegram text-info"></i> Telegram
</a>
</li>
<!-- Email -->
<li>
<a class="dropdown-item" href="#" id="share-email">
<i class="fas fa-envelope text-warning"></i> Email
</a>
</li>
<!-- Copy to clipboard -->
<li>
<a class="dropdown-item" href="#" id="copy-qr-link">
<i class="fas fa-copy text-secondary"></i> Copiar Link
</a>
</li>
<!-- Save to gallery (mobile only) -->
<li class="d-none" id="save-gallery-option">
<a class="dropdown-item" href="#" id="save-to-gallery">
<i class="fas fa-images text-purple"></i> Salvar na Galeria
</a>
</li>
</ul>
</div>
@if (User.Identity.IsAuthenticated)
{
<button id="save-to-history" class="btn btn-outline-primary w-100">
<i class="fas fa-save"></i> Salvar no Histórico
</button>
}
else
{
<div class="text-center">
<small class="text-muted">
<a href="/Account/Login" class="text-primary">Faça login</a>
para salvar no histórico
</small>
</div>
}
</div>
</div>
</div>
<!-- Premium Card for non-premium users -->
@if (User.Identity.IsAuthenticated && await AdService.ShouldShowAds(userId))
{
<div class="card border-warning mb-4">
<div class="card-header bg-warning text-dark">
<h6 class="mb-0">
<i class="fas fa-rocket"></i> QR Rapido Premium
</h6>
</div>
<div class="card-body">
<div class="text-center mb-3">
<div class="badge bg-success mb-2">⚡ 3x Mais Rápido</div>
</div>
<ul class="list-unstyled">
<li><i class="fas fa-check text-success"></i> Sem anúncios para sempre</li>
<li><i class="fas fa-check text-success"></i> QR codes ilimitados</li>
<li><i class="fas fa-check text-success"></i> Geração prioritária (0.4s)</li>
<li><i class="fas fa-check text-success"></i> QR codes dinâmicos</li>
<li><i class="fas fa-check text-success"></i> Analytics em tempo real</li>
<li><i class="fas fa-check text-success"></i> API para desenvolvedores</li>
</ul>
<div class="text-center">
<a href="/Premium/Upgrade" class="btn btn-warning w-100">
<i class="fas fa-bolt"></i> Acelerar por R$ 19,90/mês
</a>
<small class="text-muted d-block mt-1">Cancele quando quiser</small>
</div>
</div>
</div>
}
<!-- Speed Tips Card -->
<div class="card bg-light mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb text-warning"></i> Dicas para QR Mais Rápidos
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled small">
<li><i class="fas fa-arrow-right text-primary"></i> URLs curtas geram mais rápido</li>
<li><i class="fas fa-arrow-right text-primary"></i> Menos texto = maior velocidade</li>
<li><i class="fas fa-arrow-right text-primary"></i> Cores sólidas otimizam o processo</li>
<li><i class="fas fa-arrow-right text-primary"></i> Tamanhos menores aceleram o download</li>
</ul>
</div>
</div>
<!-- Ad Space Sidebar (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "sidebar" })
</div>
</div>
</div>
<!-- Speed Comparison Section -->
<section class="mt-5 mb-4">
<div class="container">
<div class="text-center mb-4">
<h3><i class="fas fa-tachometer-alt text-primary"></i> Por que QR Rapido é mais rápido?</h3>
<p class="text-muted">Comparação com outros geradores populares</p>
</div>
<div class="row">
<div class="col-md-3">
<div class="card h-100 border-success">
<div class="card-body text-center">
<h5 class="text-success">QR Rapido</h5>
<div class="display-4 text-success fw-bold">1.2s</div>
<p class="text-muted">Otimizado para velocidade</p>
<i class="fas fa-crown text-warning"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="text-muted">Concorrente A</h5>
<div class="display-4 text-muted">3.5s</div>
<p class="text-muted">Gerador tradicional</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="text-muted">Concorrente B</h5>
<div class="display-4 text-muted">4.8s</div>
<p class="text-muted">Interface pesada</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="text-muted">Concorrente C</h5>
<div class="display-4 text-muted">6.2s</div>
<p class="text-muted">Muitos anúncios</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Ad Space Footer (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "footer" })

View File

@ -0,0 +1,309 @@
@model QRRapidoApp.Models.ViewModels.UpgradeViewModel
@{
ViewData["Title"] = "QR Rapido Premium";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Hero Section -->
<div class="text-center mb-5">
<h1 class="display-4 text-gradient">
<i class="fas fa-rocket"></i> QR Rapido Premium
</h1>
<p class="lead text-muted">
Acelere sua produtividade com o gerador de QR mais rápido do mundo
</p>
<div class="badge bg-success fs-6 p-2">
<i class="fas fa-bolt"></i> 3x mais rápido que a concorrência
</div>
</div>
<!-- Current Status -->
@if (Model.IsAdFreeActive)
{
<div class="alert alert-info border-0 shadow-sm mb-4">
<div class="row align-items-center">
<div class="col-md-8">
<h6><i class="fas fa-info-circle"></i> Status Atual</h6>
<p class="mb-0">
Você tem <strong>@Model.DaysUntilAdExpiry dias</strong> restantes sem anúncios.
Upgrade agora e tenha acesso premium para sempre!
</p>
</div>
<div class="col-md-4 text-end">
<div class="badge bg-success p-2">
@Model.DaysUntilAdExpiry dias restantes
</div>
</div>
</div>
</div>
}
<!-- Pricing Card -->
<div class="row justify-content-center mb-5">
<div class="col-md-6">
<div class="card shadow-lg border-warning">
<div class="card-header bg-warning text-dark text-center">
<h3 class="mb-0">
<i class="fas fa-crown"></i> QR Rapido Premium
</h3>
<small>O plano mais popular</small>
</div>
<div class="card-body text-center">
<div class="display-3 text-warning fw-bold mb-2">
R$ @Model.PremiumPrice.ToString("0.00")
</div>
<p class="text-muted">por mês</p>
<div class="list-group list-group-flush mb-4">
<div class="list-group-item border-0">
<i class="fas fa-infinity text-success me-2"></i>
<strong>QR codes ilimitados</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-bolt text-success me-2"></i>
<strong>Geração ultra-rápida (0.4s)</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-ban text-success me-2"></i>
<strong>Sem anúncios para sempre</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-magic text-success me-2"></i>
<strong>QR codes dinâmicos</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-chart-line text-success me-2"></i>
<strong>Analytics em tempo real</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-headset text-success me-2"></i>
<strong>Suporte prioritário</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-code text-success me-2"></i>
<strong>API para desenvolvedores</strong>
</div>
</div>
<button id="upgrade-btn" class="btn btn-warning btn-lg w-100 mb-3">
<i class="fas fa-rocket"></i> Fazer Upgrade Agora
<div class="spinner-border spinner-border-sm ms-2 d-none" role="status"></div>
</button>
<small class="text-muted">
<i class="fas fa-shield-alt"></i> Pagamento seguro via Stripe
<br>
<i class="fas fa-times-circle"></i> Cancele quando quiser
</small>
</div>
</div>
</div>
</div>
<!-- Feature Comparison -->
<div class="card shadow-sm mb-5">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-balance-scale"></i> Comparação de Planos
</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Recurso</th>
<th class="text-center">Free</th>
<th class="text-center bg-warning text-dark">Premium</th>
</tr>
</thead>
<tbody>
<tr>
<td>QR codes por dia</td>
<td class="text-center">50</td>
<td class="text-center"><i class="fas fa-infinity text-success"></i> Ilimitado</td>
</tr>
<tr>
<td>Velocidade de geração</td>
<td class="text-center">1.2s</td>
<td class="text-center"><strong class="text-success">0.4s</strong></td>
</tr>
<tr>
<td>Anúncios</td>
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
<td class="text-center"><i class="fas fa-check text-success"></i> Sem anúncios</td>
</tr>
<tr>
<td>QR codes dinâmicos</td>
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
<td class="text-center"><i class="fas fa-check text-success"></i></td>
</tr>
<tr>
<td>Analytics detalhados</td>
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
<td class="text-center"><i class="fas fa-check text-success"></i></td>
</tr>
<tr>
<td>Suporte prioritário</td>
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
<td class="text-center"><i class="fas fa-check text-success"></i></td>
</tr>
<tr>
<td>API access</td>
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
<td class="text-center"><i class="fas fa-check text-success"></i></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Speed Demonstration -->
<div class="card shadow-sm mb-5">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="fas fa-stopwatch"></i> Demonstração de Velocidade
</h4>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-4">
<div class="card border-danger">
<div class="card-body">
<h5 class="text-danger">Concorrentes</h5>
<div class="display-4 text-danger">4.5s</div>
<p class="text-muted">Tempo médio</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-primary">
<div class="card-body">
<h5 class="text-primary">QR Rapido Free</h5>
<div class="display-4 text-primary">1.2s</div>
<p class="text-muted">3x mais rápido</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-success">
<div class="card-body">
<h5 class="text-success">QR Rapido Premium</h5>
<div class="display-4 text-success">0.4s</div>
<p class="text-muted">11x mais rápido!</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FAQ -->
<div class="card shadow-sm">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-question-circle"></i> Perguntas Frequentes
</h4>
</div>
<div class="card-body">
<div class="accordion" id="faqAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1">
Posso cancelar a qualquer momento?
</button>
</h2>
<div id="faq1" class="accordion-collapse collapse show">
<div class="accordion-body">
Sim! Você pode cancelar sua assinatura a qualquer momento. Não há taxas de cancelamento
e você manterá o acesso premium até o final do período já pago.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2">
O que são QR codes dinâmicos?
</button>
</h2>
<div id="faq2" class="accordion-collapse collapse">
<div class="accordion-body">
QR codes dinâmicos permitem que você altere o conteúdo do QR após ele ter sido criado,
sem precisar gerar um novo código. Perfeito para campanhas de marketing e uso empresarial.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3">
Como funciona o suporte prioritário?
</button>
</h2>
<div id="faq3" class="accordion-collapse collapse">
<div class="accordion-body">
Usuários premium recebem resposta em até 2 horas úteis por email,
acesso ao chat direto e suporte técnico especializado.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
document.getElementById('upgrade-btn').addEventListener('click', async function() {
const btn = this;
const spinner = btn.querySelector('.spinner-border');
btn.disabled = true;
spinner.classList.remove('d-none');
try {
const response = await fetch('/Premium/CreateCheckout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
// Track conversion attempt
if (typeof gtag !== 'undefined') {
gtag('event', 'begin_checkout', {
'event_category': 'Premium',
'value': @Model.PremiumPrice,
'currency': 'BRL'
});
}
window.location.href = result.url;
} else {
alert('Erro ao processar pagamento: ' + result.error);
}
} catch (error) {
console.error('Erro:', error);
alert('Erro ao processar pagamento. Tente novamente.');
} finally {
btn.disabled = false;
spinner.classList.add('d-none');
}
});
// Track page view
if (typeof gtag !== 'undefined') {
gtag('event', 'page_view', {
'page_title': 'Premium Upgrade',
'page_location': window.location.href
});
}
</script>
}

View File

@ -0,0 +1,83 @@
@using QRRapidoApp.Services
@model dynamic
@inject AdDisplayService AdService
@{
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
var showAds = await AdService.ShouldShowAds(userId);
var position = ViewBag.position ?? Model?.position ?? "header";
}
@if (showAds)
{
@switch (position)
{
case "header":
<div class="ad-container ad-header mb-4">
<div class="ad-label">Publicidade</div>
<ins class="adsbygoogle"
style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-XXXXXXXXXX"
data-ad-slot="XXXXXXXXXX"></ins>
</div>
break;
case "sidebar":
<div class="ad-container ad-sidebar mb-4">
<div class="ad-label">Publicidade</div>
<ins class="adsbygoogle"
style="display:inline-block;width:300px;height:250px"
data-ad-client="ca-pub-XXXXXXXXXX"
data-ad-slot="YYYYYYYYYY"></ins>
</div>
break;
case "footer":
<div class="ad-container ad-footer mt-5 mb-4">
<div class="ad-label">Publicidade</div>
<ins class="adsbygoogle"
style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-XXXXXXXXXX"
data-ad-slot="ZZZZZZZZZZ"></ins>
</div>
break;
case "content":
<div class="ad-container ad-content my-4">
<div class="ad-label">Publicidade</div>
<ins class="adsbygoogle"
style="display:block"
data-ad-client="ca-pub-XXXXXXXXXX"
data-ad-slot="WWWWWWWWWW"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
</div>
break;
}
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
}
else if (User.Identity.IsAuthenticated)
{
var isPremium = await AdService.HasValidPremiumSubscription(userId);
if (isPremium)
{
<!-- Premium User Message -->
<div class="alert alert-success ad-free-notice mb-3">
<i class="fas fa-crown text-warning"></i>
<span><strong>✨ Usuário Premium - Sem anúncios!</strong></span>
</div>
}
else
{
<!-- Upgrade to Premium Message -->
<div class="alert alert-info upgrade-notice mb-3">
<i class="fas fa-star text-warning"></i>
<span><strong>Faça upgrade para Premium e remova os anúncios!</strong></span>
<a href="/Premium/Upgrade" class="btn btn-sm btn-warning ms-2">
<i class="fas fa-crown"></i> Premium: Sem anúncios + Histórico + QR ilimitados
</a>
</div>
}
}

293
Views/Shared/_Layout.cshtml Normal file
View File

@ -0,0 +1,293 @@
@using QRRapidoApp.Services
@using Microsoft.AspNetCore.Http.Extensions
@inject AdDisplayService AdService
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - QR Rapido | Gerador QR Code Ultrarrápido</title>
<!-- SEO Meta Tags -->
<meta name="description" content="QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.">
<meta name="keywords" content="qr rapido, gerador qr rapido, qr code rapido, codigo qr rapido, qr gratis rapido, generador qr rapido, qr ultrarapido">
<meta name="author" content="QR Rapido">
<meta name="robots" content="index, follow">
<!-- Canonical URL -->
<link rel="canonical" href="@Context.Request.GetDisplayUrl()">
<!-- Hreflang for multilingual -->
<link rel="alternate" hreflang="pt-BR" href="https://qrrapido.site/pt/">
<link rel="alternate" hreflang="es" href="https://qrrapido.site/es/">
<link rel="alternate" hreflang="en" href="https://qrrapido.site/en/">
<link rel="alternate" hreflang="x-default" href="https://qrrapido.site/">
<!-- Open Graph -->
<meta property="og:title" content="QR Rapido - Gerador QR Code Ultrarrápido">
<meta property="og:description" content="Gere códigos QR em segundos! Grátis, rápido e fácil. 30 dias sem anúncios após login.">
<meta property="og:image" content="https://qrrapido.site/images/qrrapido-og-image.png">
<meta property="og:url" content="@Context.Request.GetDisplayUrl()">
<meta property="og:type" content="website">
<meta property="og:site_name" content="QR Rapido">
<meta property="og:locale" content="pt_BR">
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="QR Rapido - Gerador QR Code Ultrarrápido">
<meta name="twitter:description" content="Gere códigos QR em segundos! Grátis, rápido e fácil.">
<meta name="twitter:image" content="https://qrrapido.site/images/qrrapido-twitter-card.png">
<!-- Structured Data Schema.org -->
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "WebApplication",
"name": "QR Rapido",
"description": "Gerador de QR Code ultrarrápido em português e espanhol",
"url": "https://qrrapido.site",
"applicationCategory": "UtilityApplication",
"operatingSystem": "Web",
"author": {
"@@type": "Organization",
"name": "QR Rapido"
},
"offers": {
"@@type": "Offer",
"price": "0",
"priceCurrency": "BRL",
"description": "Geração gratuita de QR codes"
},
"aggregateRating": {
"@@type": "AggregateRating",
"ratingValue": "4.8",
"reviewCount": "2547"
},
"featureList": [
"Geração em segundos",
"Suporte multilíngue",
"Sem cadastro obrigatório",
"30 dias sem anúncios",
"Download múltiplos formatos"
]
}
</script>
<!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID', {
send_page_view: false
});
// Custom events for QR Rapido
window.trackQRGeneration = function(type, time, isPremium) {
gtag('event', 'qr_generated', {
'event_category': 'QR Generation',
'event_label': type,
'value': Math.round(parseFloat(time) * 1000),
'custom_parameters': {
'generation_time': parseFloat(time),
'user_type': isPremium ? 'premium' : 'free',
'speed_category': time < 1.0 ? 'ultra_fast' : time < 2.0 ? 'fast' : 'normal'
}
});
};
window.trackSpeedComparison = function(ourTime, competitorAvg) {
gtag('event', 'speed_comparison', {
'event_category': 'Performance',
'our_time': parseFloat(ourTime),
'competitor_avg': parseFloat(competitorAvg),
'speed_advantage': parseFloat(competitorAvg) - parseFloat(ourTime)
});
};
window.trackLanguageChange = function(from, to) {
gtag('event', 'language_change', {
'event_category': 'Localization',
'previous_language': from,
'new_language': to
});
};
</script>
<!-- AdSense -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXX"
crossorigin="anonymous"></script>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/qrrapido-theme.css" asp-append-version="true" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/images/qrrapido-favicon.svg">
<link rel="icon" type="image/png" href="/images/qrrapido-favicon-32x32.png">
<!-- Web App Manifest -->
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#007BFF">
</head>
<body>
<!-- Header with QR Rapido branding -->
<header class="navbar navbar-expand-lg navbar-light bg-white border-bottom sticky-top">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/">
<svg width="40" height="40" class="me-2" viewBox="0 0 100 100">
<!-- QR Rapido logo with speed effect -->
<rect x="10" y="10" width="80" height="80" fill="#007BFF" rx="8"/>
<rect x="20" y="20" width="15" height="15" fill="white"/>
<rect x="65" y="20" width="15" height="15" fill="white"/>
<rect x="20" y="65" width="15" height="15" fill="white"/>
<!-- Speed lines -->
<path d="M85 45 L95 45 M85 50 L92 50 M85 55 L89 55" stroke="#FF6B35" stroke-width="2"/>
</svg>
<div>
<h1 class="h4 mb-0 text-primary fw-bold">QR Rapido</h1>
<small class="text-muted" id="tagline">Gere QR codes em segundos!</small>
</div>
</a>
<div class="navbar-nav ms-auto d-flex flex-row align-items-center gap-3">
<!-- Language selector -->
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-globe"></i> <span id="current-lang">PT</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-lang="pt-BR">🇧🇷 Português</a></li>
<li><a class="dropdown-item" href="#" data-lang="es">🇪🇸 Español</a></li>
<li><a class="dropdown-item" href="#" data-lang="en">🇺🇸 English</a></li>
</ul>
</div>
<!-- Global speed timer -->
<div class="d-none d-md-block">
<small class="text-success fw-bold">
<i class="fas fa-stopwatch"></i>
<span id="avg-generation-time">1.2s</span> médio
</small>
</div>
@if (User.Identity.IsAuthenticated)
{
<div class="dropdown">
<button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-user"></i> @User.Identity.Name
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/Account/Profile">
<i class="fas fa-user-cog"></i> Perfil
</a></li>
<li><a class="dropdown-item" href="/Account/History">
<i class="fas fa-history"></i> Histórico
</a></li>
@{
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
var shouldShowAds = await AdService.ShouldShowAds(userId);
}
@if (!shouldShowAds)
{
<li><span class="dropdown-item text-success">
<i class="fas fa-crown"></i> Premium Ativo
</span></li>
}
else
{
<li><a class="dropdown-item text-warning" href="/Premium/Upgrade">
<i class="fas fa-rocket"></i> QR Rapido Premium
</a></li>
}
<li><hr class="dropdown-divider"></li>
<li>
<form method="post" action="/Account/Logout" class="d-inline">
<button type="submit" class="dropdown-item">
<i class="fas fa-sign-out-alt"></i> Sair
</button>
</form>
</li>
</ul>
</div>
}
else
{
<a href="/Account/Login" class="btn btn-primary btn-sm">
<i class="fas fa-sign-in-alt"></i> Login
</a>
<div class="d-none d-md-block">
<small class="text-success">
<i class="fas fa-gift"></i> Login = 30 dias sem anúncios!
</small>
</div>
}
</div>
</div>
</header>
<!-- Hero Section for speed -->
<section class="bg-gradient-primary text-white py-4 mb-4">
<div class="container text-center">
<h2 class="h5 mb-2">
<i class="fas fa-bolt"></i> O gerador de QR mais rápido da web
</h2>
<p class="mb-0 opacity-75">
Média de <strong>1.2 segundos</strong> por QR code • Grátis • Sem cadastro obrigatório
</p>
</div>
</section>
<!-- Ad Space Header (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "header" })
<main role="main">
@RenderBody()
</main>
<!-- Footer -->
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>QR Rapido</h5>
<p class="small">O gerador de QR codes mais rápido da web. Grátis, seguro e confiável.</p>
</div>
<div class="col-md-3">
<h6>Links Úteis</h6>
<ul class="list-unstyled">
<li><a href="/Home/Privacy" class="text-light">Privacidade</a></li>
<li><a href="/Home/Terms" class="text-light">Termos de Uso</a></li>
<li><a href="/Premium/Upgrade" class="text-warning">Premium</a></li>
</ul>
</div>
<div class="col-md-3">
<h6>Suporte</h6>
<ul class="list-unstyled">
<li><a href="mailto:contato@qrrapido.site" class="text-light">Contato</a></li>
<li><a href="/Help" class="text-light">Ajuda</a></li>
</ul>
</div>
</div>
<hr>
<div class="text-center">
<small>&copy; 2024 QR Rapido. Todos os direitos reservados.</small>
</div>
</div>
</footer>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="~/js/test.js" asp-append-version="true"></script>
<script src="~/js/qr-speed-generator.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

65
appsettings.json Normal file
View File

@ -0,0 +1,65 @@
{
"App": {
"Name": "QR Rapido",
"BaseUrl": "https://qrrapido.site",
"TaglinePT": "Gere QR codes em segundos!",
"TaglineES": "¡Genera códigos QR en segundos!",
"TaglineEN": "Generate QR codes in seconds!",
"Version": "1.0.0"
},
"ConnectionStrings": {
},
"Authentication": {
"Google": {
"ClientId": "your-google-client-id",
"ClientSecret": "your-google-client-secret"
},
"Microsoft": {
"ClientId": "your-microsoft-client-id",
"ClientSecret": "your-microsoft-client-secret"
}
},
"Stripe": {
"PublishableKey": "pk_test_xxxxx",
"SecretKey": "sk_test_xxxxx",
"WebhookSecret": "whsec_xxxxx",
"PriceId": "price_xxxxx"
},
"AdSense": {
"ClientId": "ca-pub-XXXXXXXXXX",
"Enabled": true
},
"Performance": {
"QRGenerationTimeoutMs": 2000,
"CacheExpirationMinutes": 60,
"MaxConcurrentGenerations": 100
},
"HistoryCleanup": {
"GracePeriodDays": 7,
"CleanupIntervalHours": 6
},
"Premium": {
"FreeQRLimit": 10,
"PremiumPrice": 19.90,
"Features": {
"UnlimitedQR": true,
"DynamicQR": true,
"NoAds": true,
"PrioritySupport": true,
"AdvancedAnalytics": true,
"SpeedBoost": true
}
},
"SEO": {
"KeywordsPT": "qr rapido, gerador qr rapido, qr code rapido, codigo qr rapido, qr gratis rapido",
"KeywordsES": "qr rapido, generador qr rapido, codigo qr rapido, qr gratis rapido",
"KeywordsEN": "fast qr, quick qr generator, rapid qr code, qr code generator"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

83
docker-compose.yml Normal file
View File

@ -0,0 +1,83 @@
version: '3.8'
services:
qrrapido:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:80"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__MongoDB=mongodb://mongo:27017/qrrapido
- ConnectionStrings__Redis=redis:6379
- Authentication__Google__ClientId=${GOOGLE_CLIENT_ID}
- Authentication__Google__ClientSecret=${GOOGLE_CLIENT_SECRET}
- Authentication__Microsoft__ClientId=${MICROSOFT_CLIENT_ID}
- Authentication__Microsoft__ClientSecret=${MICROSOFT_CLIENT_SECRET}
- Stripe__PublishableKey=${STRIPE_PUBLISHABLE_KEY}
- Stripe__SecretKey=${STRIPE_SECRET_KEY}
- Stripe__WebhookSecret=${STRIPE_WEBHOOK_SECRET}
- Stripe__PriceId=${STRIPE_PRICE_ID}
depends_on:
- mongo
- redis
volumes:
- ./logs:/app/logs
networks:
- qrrapido-network
restart: unless-stopped
mongo:
image: mongo:7.0
container_name: qrrapido-mongo
ports:
- "27017:27017"
environment:
- MONGO_INITDB_DATABASE=qrrapido
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=password123
volumes:
- mongo_data:/data/db
- ./scripts/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
networks:
- qrrapido-network
restart: unless-stopped
redis:
image: redis:7.2-alpine
container_name: qrrapido-redis
ports:
- "6379:6379"
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --appendonly yes
volumes:
- redis_data:/data
networks:
- qrrapido-network
restart: unless-stopped
nginx:
image: nginx:alpine
container_name: qrrapido-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- qrrapido
networks:
- qrrapido-network
restart: unless-stopped
volumes:
mongo_data:
driver: local
redis_data:
driver: local
networks:
qrrapido-network:
driver: bridge

View File

@ -0,0 +1,383 @@
/* QR Rapido Custom Theme */
:root {
--qr-primary: #007BFF;
--qr-secondary: #28A745;
--qr-accent: #FF6B35;
--qr-warning: #FFC107;
--qr-success: #28A745;
--qr-danger: #DC3545;
--qr-dark: #343A40;
--qr-light: #F8F9FA;
}
/* Global Styles */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
}
.bg-gradient-primary {
background: linear-gradient(135deg, var(--qr-primary) 0%, #0056B3 100%);
}
/* Generation Timer Styles */
.generation-timer {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
background: #f8f9fa;
border-radius: 20px;
border: 2px solid var(--qr-primary);
transition: all 0.3s ease;
}
.generation-timer.active {
background: var(--qr-primary);
color: white;
animation: pulse 1.5s infinite;
}
/* Speed Badge Animation */
.speed-badge {
animation: slideInRight 0.5s ease-out;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes slideInRight {
from {
transform: translateX(20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* QR Preview Placeholder */
.placeholder-qr {
border: 2px dashed #dee2e6;
border-radius: 8px;
transition: all 0.3s ease;
background: #f8f9fa;
}
.placeholder-qr:hover {
border-color: var(--qr-primary);
background: #e3f2fd;
}
/* Logo and Branding */
.navbar-brand svg {
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));
transition: transform 0.3s ease;
}
.navbar-brand:hover svg {
transform: scale(1.05);
}
/* QR Preview Image */
#qr-preview img {
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
max-width: 100%;
height: auto;
}
#qr-preview img:hover {
transform: scale(1.02);
}
/* Card Hover Effects */
.card {
transition: all 0.3s ease;
border-radius: 12px;
}
.card:hover {
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
/* Button Styles */
.btn-check:checked + .btn {
background-color: var(--qr-primary);
border-color: var(--qr-primary);
color: white;
}
.btn-primary {
background: linear-gradient(135deg, var(--qr-primary) 0%, #0056B3 100%);
border: none;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, #0056B3 0%, var(--qr-primary) 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
/* Form Controls */
.form-control:focus,
.form-select:focus {
border-color: var(--qr-primary);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.form-control-lg {
border-radius: 8px;
}
/* Speed Statistics Cards */
.card.border-success {
border-color: var(--qr-success) !important;
}
.card.border-primary {
border-color: var(--qr-primary) !important;
}
.card.border-warning {
border-color: var(--qr-warning) !important;
}
/* Generation Stats Animation */
.generation-stats {
animation: slideInRight 0.5s ease-out;
}
/* Ad Container Styles */
.ad-container {
text-align: center;
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
position: relative;
}
.ad-label {
font-size: 11px;
color: #6c757d;
margin-bottom: 8px;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.5px;
}
.ad-free-notice {
text-align: center;
border-left: 4px solid var(--qr-success);
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border-radius: 8px;
}
.ad-free-notice .fas {
color: var(--qr-warning);
}
/* Loading Animations */
.spinner-border-sm {
width: 1rem;
height: 1rem;
}
/* Progress Bar for QR Generation */
.progress {
height: 4px;
border-radius: 2px;
background-color: #e9ecef;
}
.progress-bar {
background: linear-gradient(90deg, var(--qr-primary), var(--qr-accent));
}
/* Speed Tips */
.list-unstyled li {
padding: 0.25rem 0;
transition: all 0.2s ease;
}
.list-unstyled li:hover {
padding-left: 0.5rem;
color: var(--qr-primary);
}
/* Responsive Design */
@media (max-width: 768px) {
.generation-timer {
font-size: 0.875rem;
padding: 0.2rem 0.5rem;
}
.speed-badge .badge {
font-size: 0.75rem;
}
.placeholder-qr {
padding: 2rem 1rem;
}
.card-body {
padding: 1rem;
}
.ad-container {
margin: 10px 0;
padding: 10px;
}
}
/* Premium Features Styling */
.premium-feature {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border-left: 4px solid var(--qr-warning);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
}
.premium-badge {
background: linear-gradient(135deg, var(--qr-warning) 0%, #f39c12 100%);
color: var(--qr-dark);
border-radius: 20px;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Accordion Customization */
.accordion-button {
background: var(--qr-light);
border: none;
border-radius: 8px !important;
}
.accordion-button:not(.collapsed) {
background: var(--qr-primary);
color: white;
}
.accordion-button:focus {
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
/* Footer Styling */
footer {
background: linear-gradient(135deg, var(--qr-dark) 0%, #2c3e50 100%);
}
footer a {
text-decoration: none;
transition: color 0.3s ease;
}
footer a:hover {
color: var(--qr-primary) !important;
}
/* Language Selector */
.dropdown-toggle::after {
margin-left: 0.5rem;
}
.dropdown-menu {
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: none;
}
.dropdown-item {
transition: all 0.2s ease;
border-radius: 4px;
margin: 2px 4px;
}
.dropdown-item:hover {
background: var(--qr-primary);
color: white;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--qr-primary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #0056b3;
}
/* Utility Classes */
.text-gradient {
background: linear-gradient(135deg, var(--qr-primary) 0%, var(--qr-accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.shadow-custom {
box-shadow: 0 8px 25px rgba(0, 123, 255, 0.15);
}
.border-gradient {
border: 2px solid;
border-image: linear-gradient(135deg, var(--qr-primary) 0%, var(--qr-accent) 100%) 1;
}
/* Print Styles */
@media print {
.ad-container,
.btn,
.navbar,
footer {
display: none !important;
}
#qr-preview img {
max-width: 300px;
max-height: 300px;
}
}
/* Dark Mode Support (future enhancement) */
@media (prefers-color-scheme: dark) {
.card {
background-color: #2d3748;
color: #e2e8f0;
}
.placeholder-qr {
background: #4a5568;
border-color: #718096;
}
.form-control,
.form-select {
background-color: #4a5568;
border-color: #718096;
color: #e2e8f0;
}
}

126
wwwroot/css/site.css Normal file
View File

@ -0,0 +1,126 @@
/* Site test CSS */
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.test-style {
color: red;
font-weight: bold;
}
/* Ensure Bootstrap and FontAwesome work */
.btn {
border-radius: 0.375rem;
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
/* Share Button Styles */
#share-qr-btn {
position: relative;
transition: all 0.3s ease;
}
#share-qr-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
#share-dropdown {
min-width: 250px;
border: none;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
#share-dropdown .dropdown-item {
padding: 12px 20px;
border-bottom: 1px solid #f1f3f4;
transition: all 0.2s ease;
}
#share-dropdown .dropdown-item:last-child {
border-bottom: none;
}
#share-dropdown .dropdown-item:hover {
background: linear-gradient(45deg, #f8f9fa, #e9ecef);
padding-left: 25px;
transform: translateX(5px);
}
#share-dropdown .dropdown-item i {
width: 20px;
margin-right: 12px;
font-size: 16px;
}
/* Mobile optimizations for share button */
@media (max-width: 768px) {
#share-dropdown {
min-width: 100%;
margin-top: 5px;
}
#share-dropdown .dropdown-item {
padding: 15px 20px;
font-size: 16px;
}
#share-dropdown .dropdown-item i {
font-size: 18px;
margin-right: 15px;
}
#share-qr-btn {
font-size: 16px;
padding: 12px 20px;
}
}
/* Share button loading state */
#share-qr-btn.loading {
pointer-events: none;
opacity: 0.7;
}
#share-qr-btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
top: 50%;
left: 50%;
margin-left: -8px;
margin-top: -8px;
border: 2px solid transparent;
border-top: 2px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Special colors for social platforms */
.text-purple {
color: #6f42c1 !important;
}
/* Share success feedback */
.share-success {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.share-success.show {
transform: translateX(0);
}

View File

@ -0,0 +1,874 @@
// QR Rapido Speed Generator
class QRRapidoGenerator {
constructor() {
this.startTime = 0;
this.currentQR = null;
this.timerInterval = null;
this.languageStrings = {
'pt-BR': {
tagline: 'Gere QR codes em segundos!',
generating: 'Gerando...',
generated: 'Gerado em',
seconds: 's',
ultraFast: 'Geração ultra rápida!',
fast: 'Geração rápida!',
normal: 'Geração normal',
error: 'Erro na geração. Tente novamente.',
success: 'QR Code salvo no histórico!'
},
'es': {
tagline: '¡Genera códigos QR en segundos!',
generating: 'Generando...',
generated: 'Generado en',
seconds: 's',
ultraFast: '¡Generación ultra rápida!',
fast: '¡Generación rápida!',
normal: 'Generación normal',
error: 'Error en la generación. Inténtalo de nuevo.',
success: '¡Código QR guardado en el historial!'
},
'en': {
tagline: 'Generate QR codes in seconds!',
generating: 'Generating...',
generated: 'Generated in',
seconds: 's',
ultraFast: 'Ultra fast generation!',
fast: 'Fast generation!',
normal: 'Normal generation',
error: 'Generation error. Please try again.',
success: 'QR Code saved to history!'
}
};
this.currentLang = localStorage.getItem('qrrapido-lang') || 'pt-BR';
this.initializeEvents();
this.checkAdFreeStatus();
this.updateLanguage();
this.updateStatsCounters();
}
initializeEvents() {
// Form submission with timer
const form = document.getElementById('qr-speed-form');
if (form) {
form.addEventListener('submit', this.generateQRWithTimer.bind(this));
}
// Quick style selection
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
radio.addEventListener('change', this.applyQuickStyle.bind(this));
});
// QR type change with hints
const qrType = document.getElementById('qr-type');
if (qrType) {
qrType.addEventListener('change', this.updateContentHints.bind(this));
}
// Language selector
document.querySelectorAll('[data-lang]').forEach(link => {
link.addEventListener('click', this.changeLanguage.bind(this));
});
// Real-time preview for premium users
if (this.isPremiumUser()) {
this.setupRealTimePreview();
}
// Download buttons
this.setupDownloadButtons();
// Share functionality
this.setupShareButtons();
// Save to history
const saveBtn = document.getElementById('save-to-history');
if (saveBtn) {
saveBtn.addEventListener('click', this.saveToHistory.bind(this));
}
}
setupDownloadButtons() {
const pngBtn = document.getElementById('download-png');
const svgBtn = document.getElementById('download-svg');
const pdfBtn = document.getElementById('download-pdf');
if (pngBtn) pngBtn.addEventListener('click', () => this.downloadQR('png'));
if (svgBtn) svgBtn.addEventListener('click', () => this.downloadQR('svg'));
if (pdfBtn) pdfBtn.addEventListener('click', () => this.downloadQR('pdf'));
}
setupShareButtons() {
// Check if Web Share API is supported and show/hide native share option
if (navigator.share && this.isMobileDevice()) {
const nativeShareOption = document.getElementById('native-share-option');
if (nativeShareOption) {
nativeShareOption.classList.remove('d-none');
}
}
// Show save to gallery option on mobile
if (this.isMobileDevice()) {
const saveGalleryOption = document.getElementById('save-gallery-option');
if (saveGalleryOption) {
saveGalleryOption.classList.remove('d-none');
}
}
// Add event listeners to share buttons
const shareButtons = {
'native-share': () => this.shareNative(),
'share-whatsapp': () => this.shareWhatsApp(),
'share-telegram': () => this.shareTelegram(),
'share-email': () => this.shareEmail(),
'copy-qr-link': () => this.copyToClipboard(),
'save-to-gallery': () => this.saveToGallery()
};
Object.entries(shareButtons).forEach(([id, handler]) => {
const button = document.getElementById(id);
if (button) {
button.addEventListener('click', (e) => {
e.preventDefault();
handler();
});
}
});
}
isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
}
async shareNative() {
if (!this.currentQR || !navigator.share) return;
try {
// Create a blob from the base64 image
const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`);
const blob = await base64Response.blob();
const file = new File([blob], 'qrcode.png', { type: 'image/png' });
const shareData = {
title: 'QR Code - QR Rapido',
text: 'QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!',
url: window.location.origin,
files: [file]
};
// Check if files can be shared
if (navigator.canShare && navigator.canShare(shareData)) {
await navigator.share(shareData);
} else {
// Fallback without files
await navigator.share({
title: shareData.title,
text: shareData.text,
url: shareData.url
});
}
this.trackShareEvent('native');
} catch (error) {
console.error('Error sharing:', error);
if (error.name !== 'AbortError') {
this.showError('Erro ao compartilhar. Tente outro método.');
}
}
}
shareWhatsApp() {
if (!this.currentQR) return;
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil! ' + window.location.origin);
const url = `https://wa.me/?text=${text}`;
if (this.isMobileDevice()) {
window.open(url, '_blank');
} else {
window.open(`https://web.whatsapp.com/send?text=${text}`, '_blank');
}
this.trackShareEvent('whatsapp');
}
shareTelegram() {
if (!this.currentQR) return;
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!');
const url = encodeURIComponent(window.location.origin);
const telegramUrl = `https://t.me/share/url?url=${url}&text=${text}`;
window.open(telegramUrl, '_blank');
this.trackShareEvent('telegram');
}
shareEmail() {
if (!this.currentQR) return;
const subject = encodeURIComponent('QR Code - QR Rapido');
const body = encodeURIComponent(`Olá!\n\nCompartilho com você este QR Code gerado no QR Rapido, o gerador mais rápido do Brasil!\n\nAcesse: ${window.location.origin}\n\nAbraços!`);
const mailtoUrl = `mailto:?subject=${subject}&body=${body}`;
window.location.href = mailtoUrl;
this.trackShareEvent('email');
}
async copyToClipboard() {
if (!this.currentQR) return;
try {
const shareText = `QR Code gerado com QR Rapido - ${window.location.origin}`;
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(shareText);
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = shareText;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
textArea.remove();
}
this.showSuccess('Link copiado para a área de transferência!');
this.trackShareEvent('copy');
} catch (error) {
console.error('Error copying to clipboard:', error);
this.showError('Erro ao copiar link. Tente novamente.');
}
}
async saveToGallery() {
if (!this.currentQR) return;
try {
// Create a blob from the base64 image
const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`);
const blob = await base64Response.blob();
// Create download link
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `qrrapido-${new Date().toISOString().slice(0,10)}-${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.showSuccess('QR Code baixado! Verifique sua galeria/downloads.');
this.trackShareEvent('gallery');
} catch (error) {
console.error('Error saving to gallery:', error);
this.showError('Erro ao salvar na galeria. Tente novamente.');
}
}
trackShareEvent(method) {
// Google Analytics
if (typeof gtag !== 'undefined') {
gtag('event', 'qr_shared', {
'share_method': method,
'user_type': this.isPremiumUser() ? 'premium' : 'free',
'language': this.currentLang
});
}
// Internal tracking
console.log(`QR Code shared via ${method}`);
}
async generateQRWithTimer(e) {
e.preventDefault();
// Validation
if (!this.validateForm()) return;
// Start timer
this.startTime = performance.now();
this.showGenerationStarted();
const formData = this.collectFormData();
try {
const response = await fetch('/api/QR/GenerateRapid', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
if (!response.ok) {
if (response.status === 429) {
this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.');
return;
}
throw new Error('Erro na geração');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Erro desconhecido');
}
const generationTime = ((performance.now() - this.startTime) / 1000).toFixed(1);
this.displayQRResult(result, generationTime);
this.updateSpeedStats(generationTime);
this.trackGenerationEvent(formData.type, generationTime);
} catch (error) {
console.error('Erro ao gerar QR:', error);
this.showError(this.languageStrings[this.currentLang].error);
} finally {
this.hideGenerationLoading();
}
}
validateForm() {
const qrType = document.getElementById('qr-type').value;
const qrContent = document.getElementById('qr-content').value.trim();
if (!qrType) {
this.showError('Selecione o tipo de QR code');
return false;
}
if (!qrContent) {
this.showError('Digite o conteúdo do QR code');
return false;
}
if (qrContent.length > 4000) {
this.showError('Conteúdo muito longo. Máximo 4000 caracteres.');
return false;
}
return true;
}
collectFormData() {
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
const styleSettings = this.getStyleSettings(quickStyle);
return {
type: document.getElementById('qr-type').value,
content: document.getElementById('qr-content').value,
quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value,
backgroundColor: document.getElementById('bg-color').value,
size: parseInt(document.getElementById('qr-size').value),
margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true,
language: this.currentLang,
...styleSettings
};
}
getStyleSettings(style) {
const styles = {
classic: { primaryColor: '#000000', backgroundColor: '#FFFFFF' },
modern: { primaryColor: '#007BFF', backgroundColor: '#F8F9FA' },
colorful: { primaryColor: '#FF6B35', backgroundColor: '#FFF3E0' }
};
return styles[style] || styles.classic;
}
displayQRResult(result, generationTime) {
const previewDiv = document.getElementById('qr-preview');
if (!previewDiv) return;
previewDiv.innerHTML = `
<img src="data:image/png;base64,${result.qrCodeBase64}"
class="img-fluid border rounded shadow-sm"
alt="QR Code gerado em ${generationTime}s">
`;
// Show generation statistics
this.showGenerationStats(generationTime);
// Show download buttons
const downloadSection = document.getElementById('download-section');
if (downloadSection) {
downloadSection.style.display = 'block';
}
// Save current data
this.currentQR = {
base64: result.qrCodeBase64,
id: result.qrId,
generationTime: generationTime
};
// Update counter for free users
if (result.remainingQRs !== undefined) {
this.updateRemainingCounter(result.remainingQRs);
}
}
showGenerationStarted() {
const button = document.getElementById('generate-btn');
const spinner = button?.querySelector('.spinner-border');
const timer = document.querySelector('.generation-timer');
if (button) button.disabled = true;
if (spinner) spinner.classList.remove('d-none');
if (timer) timer.classList.remove('d-none');
// Update timer in real time
this.timerInterval = setInterval(() => {
const elapsed = ((performance.now() - this.startTime) / 1000).toFixed(1);
const timerSpan = timer?.querySelector('span');
if (timerSpan) {
timerSpan.textContent = `${elapsed}s`;
}
}, 100);
// Preview loading
const preview = document.getElementById('qr-preview');
if (preview) {
preview.innerHTML = `
<div class="text-center p-4">
<div class="spinner-border text-primary mb-3" role="status"></div>
<p class="text-muted">${this.languageStrings[this.currentLang].generating}</p>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
style="width: 100%"></div>
</div>
</div>
`;
}
}
showGenerationStats(generationTime) {
const statsDiv = document.querySelector('.generation-stats');
const speedBadge = document.querySelector('.speed-badge');
if (statsDiv) {
statsDiv.classList.remove('d-none');
const timeSpan = statsDiv.querySelector('.generation-time');
if (timeSpan) {
timeSpan.textContent = `${generationTime}s`;
}
}
// Show speed badge
if (speedBadge) {
const strings = this.languageStrings[this.currentLang];
let badgeText = strings.normal;
let badgeClass = 'bg-secondary';
if (generationTime < 1.0) {
badgeText = strings.ultraFast;
badgeClass = 'bg-success';
} else if (generationTime < 2.0) {
badgeText = strings.fast;
badgeClass = 'bg-primary';
}
speedBadge.innerHTML = `
<span class="badge ${badgeClass}">
<i class="fas fa-bolt"></i> ${badgeText}
</span>
`;
speedBadge.classList.remove('d-none');
}
}
hideGenerationLoading() {
const button = document.getElementById('generate-btn');
const spinner = button?.querySelector('.spinner-border');
if (button) button.disabled = false;
if (spinner) spinner.classList.add('d-none');
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
updateContentHints() {
const type = document.getElementById('qr-type')?.value;
const hintsElement = document.getElementById('content-hints');
if (!hintsElement || !type) return;
const hints = {
'pt-BR': {
'url': 'Ex: https://www.exemplo.com.br',
'text': 'Digite qualquer texto que desejar',
'wifi': 'Nome da rede;Senha;Tipo de segurança (WPA/WEP)',
'vcard': 'Nome;Telefone;Email;Empresa',
'sms': 'Número;Mensagem',
'email': 'email@exemplo.com;Assunto;Mensagem'
},
'es': {
'url': 'Ej: https://www.ejemplo.com',
'text': 'Escribe cualquier texto que desees',
'wifi': 'Nombre de red;Contraseña;Tipo de seguridad (WPA/WEP)',
'vcard': 'Nombre;Teléfono;Email;Empresa',
'sms': 'Número;Mensaje',
'email': 'email@ejemplo.com;Asunto;Mensaje'
}
};
const langHints = hints[this.currentLang] || hints['pt-BR'];
hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado';
}
changeLanguage(e) {
e.preventDefault();
this.currentLang = e.target.dataset.lang;
this.updateLanguage();
this.updateContentHints();
// Save preference
localStorage.setItem('qrrapido-lang', this.currentLang);
// Track language change
window.trackLanguageChange && window.trackLanguageChange('pt-BR', this.currentLang);
}
updateLanguage() {
const strings = this.languageStrings[this.currentLang];
// Update tagline
const tagline = document.getElementById('tagline');
if (tagline) {
tagline.textContent = strings.tagline;
}
// Update language selector
const langMap = { 'pt-BR': 'PT', 'es': 'ES', 'en': 'EN' };
const currentLang = document.getElementById('current-lang');
if (currentLang) {
currentLang.textContent = langMap[this.currentLang];
}
// Update hints if type already selected
const qrType = document.getElementById('qr-type');
if (qrType?.value) {
this.updateContentHints();
}
}
applyQuickStyle(e) {
const style = e.target.value;
const settings = this.getStyleSettings(style);
const primaryColor = document.getElementById('primary-color');
const bgColor = document.getElementById('bg-color');
if (primaryColor) primaryColor.value = settings.primaryColor;
if (bgColor) bgColor.value = settings.backgroundColor;
}
updateStatsCounters() {
// Simulate real-time counters
setInterval(() => {
const totalElement = document.getElementById('total-qrs');
if (totalElement) {
const current = parseFloat(totalElement.textContent.replace('K', '')) || 10.5;
const newValue = (current + Math.random() * 0.1).toFixed(1);
totalElement.textContent = `${newValue}K`;
}
// Update average time based on real performance
const avgElement = document.getElementById('avg-generation-time');
if (avgElement && window.qrRapidoStats) {
const avg = window.qrRapidoStats.getAverageTime();
avgElement.textContent = `${avg}s`;
}
}, 30000); // Update every 30 seconds
}
trackGenerationEvent(type, time) {
// Google Analytics
if (typeof gtag !== 'undefined') {
gtag('event', 'qr_generated', {
'qr_type': type,
'generation_time': parseFloat(time),
'user_type': this.isPremiumUser() ? 'premium' : 'free',
'language': this.currentLang
});
}
// Internal statistics
if (!window.qrRapidoStats) {
window.qrRapidoStats = {
times: [],
getAverageTime: function() {
if (this.times.length === 0) return '1.2';
const avg = this.times.reduce((a, b) => a + b) / this.times.length;
return avg.toFixed(1);
}
};
}
window.qrRapidoStats.times.push(parseFloat(time));
}
isPremiumUser() {
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
}
async downloadQR(format) {
if (!this.currentQR) return;
try {
const response = await fetch(`/api/QR/Download/${this.currentQR.id}?format=${format}`);
if (!response.ok) throw new Error('Download failed');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `qrrapido-${new Date().toISOString().slice(0,10)}.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Download error:', error);
this.showError('Erro ao fazer download. Tente novamente.');
}
}
async saveToHistory() {
if (!this.currentQR) return;
try {
const response = await fetch('/api/QR/SaveToHistory', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
qrId: this.currentQR.id
})
});
if (response.ok) {
this.showSuccess(this.languageStrings[this.currentLang].success);
} else {
throw new Error('Failed to save');
}
} catch (error) {
console.error('Save error:', error);
this.showError('Erro ao salvar no histórico.');
}
}
async checkAdFreeStatus() {
try {
const response = await fetch('/Account/AdFreeStatus');
const status = await response.json();
if (status.isAdFree) {
this.hideAllAds();
this.showAdFreeMessage(status.timeRemaining);
}
} catch (error) {
console.error('Error checking ad-free status:', error);
}
}
hideAllAds() {
document.querySelectorAll('.ad-container').forEach(ad => {
ad.style.display = 'none';
});
}
showAdFreeMessage(timeRemaining) {
if (timeRemaining <= 0) return;
const existing = document.querySelector('.ad-free-notice');
if (existing) return; // Already shown
const message = document.createElement('div');
message.className = 'alert alert-success text-center mb-3 ad-free-notice';
message.innerHTML = `
<i class="fas fa-crown text-warning"></i>
<strong>Sessão sem anúncios ativa!</strong>
Tempo restante: <span class="ad-free-countdown">${this.formatTime(timeRemaining)}</span>
<a href="/Premium/Upgrade" class="btn btn-sm btn-warning ms-2">Tornar Permanente</a>
`;
const container = document.querySelector('.container');
const row = container?.querySelector('.row');
if (container && row) {
container.insertBefore(message, row);
}
}
formatTime(minutes) {
if (minutes === 0) return '0m';
const days = Math.floor(minutes / 1440);
const hours = Math.floor((minutes % 1440) / 60);
const mins = minutes % 60;
if (days > 0) return `${days}d ${hours}h ${mins}m`;
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
showUpgradeModal(message) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title">
<i class="fas fa-crown"></i> Upgrade para Premium
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>${message}</p>
<div class="row">
<div class="col-md-6">
<h6>Plano Atual (Free)</h6>
<ul class="list-unstyled">
<li> Limite de 10 QR/dia</li>
<li> Anúncios</li>
<li> QR básicos</li>
</ul>
</div>
<div class="col-md-6">
<h6>Premium (R$ 19,90/mês)</h6>
<ul class="list-unstyled">
<li> QR ilimitados</li>
<li> Sem anúncios</li>
<li> QR dinâmicos</li>
<li> Analytics</li>
<li> Suporte prioritário</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<a href="/Premium/Upgrade" class="btn btn-warning">
<i class="fas fa-crown"></i> Fazer Upgrade
</a>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
// Remove modal when closed
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
});
}
updateRemainingCounter(remaining) {
const counterElement = document.querySelector('.qr-counter');
if (counterElement) {
counterElement.textContent = `${remaining} QR codes restantes hoje`;
if (remaining <= 3) {
counterElement.className = 'badge bg-warning qr-counter';
}
if (remaining === 0) {
counterElement.className = 'badge bg-danger qr-counter';
}
}
}
showError(message) {
this.showAlert(message, 'danger');
}
showSuccess(message) {
this.showAlert(message, 'success');
}
showAlert(message, type) {
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container');
const row = container?.querySelector('.row');
if (container && row) {
container.insertBefore(alert, row);
}
// Auto-remove after delay
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, type === 'success' ? 3000 : 5000);
}
setupRealTimePreview() {
const contentField = document.getElementById('qr-content');
const typeField = document.getElementById('qr-type');
if (contentField && typeField) {
let previewTimeout;
const updatePreview = () => {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(() => {
if (contentField.value.trim() && typeField.value) {
// Could implement real-time preview for premium users
console.log('Real-time preview update');
}
}, 500);
};
contentField.addEventListener('input', updatePreview);
typeField.addEventListener('change', updatePreview);
}
}
}
// Initialize when DOM loads
document.addEventListener('DOMContentLoaded', () => {
window.qrGenerator = new QRRapidoGenerator();
// Initialize AdSense if necessary
if (window.adsbygoogle && document.querySelector('.adsbygoogle')) {
(adsbygoogle = window.adsbygoogle || []).push({});
}
});
// Global functions for ad control
window.QRApp = {
refreshAds: function() {
if (window.adsbygoogle) {
document.querySelectorAll('.adsbygoogle').forEach(ad => {
(adsbygoogle = window.adsbygoogle || []).push({});
});
}
},
hideAds: function() {
document.querySelectorAll('.ad-container').forEach(ad => {
ad.style.display = 'none';
});
}
};

13
wwwroot/js/test.js Normal file
View File

@ -0,0 +1,13 @@
// Test JavaScript file
console.log('JavaScript loaded successfully!');
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded!');
// Add test class to body
document.body.classList.add('js-loaded');
// Test if elements exist
console.log('Bootstrap classes found:', document.querySelector('.container') !== null);
console.log('FontAwesome icons found:', document.querySelector('.fas') !== null);
});

40
wwwroot/manifest.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "QR Rapido - Gerador QR Code Ultrarrápido",
"short_name": "QR Rapido",
"description": "Gere códigos QR em segundos! Grátis e ultrarrápido.",
"start_url": "/",
"display": "standalone",
"background_color": "#FFFFFF",
"theme_color": "#007BFF",
"orientation": "portrait-primary",
"icons": [
{
"src": "/images/qrrapido-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/qrrapido-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"categories": ["utilities", "productivity"],
"shortcuts": [
{
"name": "Gerar QR URL",
"short_name": "QR URL",
"description": "Criar QR code para URL",
"url": "/?type=url",
"icons": [{"src": "/images/shortcut-url.png", "sizes": "96x96"}]
},
{
"name": "Gerar QR Texto",
"short_name": "QR Texto",
"description": "Criar QR code para texto",
"url": "/?type=text",
"icons": [{"src": "/images/shortcut-text.png", "sizes": "96x96"}]
}
]
}