Compare commits

..

No commits in common. "eb0751cb162a78b1dcc0a533437949ce86d17175" and "2edb4e119625fa6ed9dfa3935ec7a6a9bb77d54f" have entirely different histories.

17 changed files with 28 additions and 916 deletions

View File

@ -31,9 +31,7 @@
"Read(//mnt/c/vscode/**)", "Read(//mnt/c/vscode/**)",
"Read(//mnt/c/**)", "Read(//mnt/c/**)",
"Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)", "Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)",
"Bash(netstat -tln)", "Bash(netstat -tln)"
"Bash(npm install)",
"Bash(npm install:*)"
], ],
"deny": [] "deny": []
} }

View File

@ -307,8 +307,8 @@ namespace QRRapidoApp.Controllers
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false; var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
// DEBUG: Log detalhado dos parâmetros recebidos // DEBUG: Log detalhado dos parâmetros recebidos
_logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, EnableTracking: {EnableTracking}, Type: {Type}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}", _logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}",
requestId, request.EnableTracking, request.Type, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo); requestId, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo);
using (_logger.BeginScope(new Dictionary<string, object> using (_logger.BeginScope(new Dictionary<string, object>
{ {

View File

@ -1,138 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Services;
using System.Diagnostics;
namespace QRRapidoApp.Controllers
{
/// <summary>
/// Controller for tracking QR code scans (Premium feature)
/// Handles redirect URLs and analytics
/// </summary>
[Route("t")]
public class TrackingController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<TrackingController> _logger;
public TrackingController(IUserService userService, ILogger<TrackingController> logger)
{
_userService = userService;
_logger = logger;
}
/// <summary>
/// Track QR code scan and redirect to original URL
/// URL format: https://qrrapido.site/t/{trackingId}
/// </summary>
[HttpGet("{trackingId}")]
public async Task<IActionResult> TrackAndRedirect(string trackingId)
{
var stopwatch = Stopwatch.StartNew();
using (_logger.BeginScope(new Dictionary<string, object>
{
["TrackingId"] = trackingId,
["QRTracking"] = true
}))
{
_logger.LogInformation("QR tracking request - TrackingId: {TrackingId}", trackingId);
try
{
// Get QR code from database
var qr = await _userService.GetQRByTrackingIdAsync(trackingId);
if (qr == null)
{
_logger.LogWarning("QR code not found for tracking - TrackingId: {TrackingId}", trackingId);
return NotFound("QR code not found");
}
// Increment scan count and update last access (fire and forget for performance)
_ = Task.Run(async () =>
{
try
{
await _userService.IncrementQRScanCountAsync(trackingId);
_logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}, NewCount: {NewCount}",
trackingId, qr.ScanCount + 1);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error incrementing scan count - TrackingId: {TrackingId}", trackingId);
}
});
stopwatch.Stop();
_logger.LogInformation("QR redirect - TrackingId: {TrackingId}, Destination: {Destination}, ProcessingTime: {ProcessingTimeMs}ms",
trackingId, qr.Content, stopwatch.ElapsedMilliseconds);
// Redirect to original URL (HTTP 302 temporary redirect)
return Redirect(qr.Content);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "QR tracking failed - TrackingId: {TrackingId}, ProcessingTime: {ProcessingTimeMs}ms",
trackingId, stopwatch.ElapsedMilliseconds);
// Return error page instead of exposing exception details
return StatusCode(500, "Error processing QR code");
}
}
}
/// <summary>
/// Get analytics for a specific QR code (requires authentication)
/// </summary>
[HttpGet("analytics/{trackingId}")]
public async Task<IActionResult> GetAnalytics(string trackingId)
{
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
_logger.LogWarning("Analytics request without authentication - TrackingId: {TrackingId}", trackingId);
return Unauthorized();
}
try
{
var qr = await _userService.GetQRByTrackingIdAsync(trackingId);
if (qr == null)
{
_logger.LogWarning("Analytics request for non-existent QR - TrackingId: {TrackingId}", trackingId);
return NotFound();
}
// Verify ownership
if (qr.UserId != userId)
{
_logger.LogWarning("Analytics request for QR owned by different user - TrackingId: {TrackingId}, RequestingUser: {UserId}, Owner: {OwnerId}",
trackingId, userId, qr.UserId);
return Forbid();
}
_logger.LogInformation("Analytics retrieved - TrackingId: {TrackingId}, UserId: {UserId}, ScanCount: {ScanCount}",
trackingId, userId, qr.ScanCount);
return Ok(new
{
trackingId = qr.TrackingId,
scanCount = qr.ScanCount,
createdAt = qr.CreatedAt,
lastAccessedAt = qr.LastAccessedAt,
content = qr.Content,
type = qr.Type
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving analytics - TrackingId: {TrackingId}, UserId: {UserId}",
trackingId, userId);
return StatusCode(500);
}
}
}
}

View File

@ -1,345 +0,0 @@
# 📊 QR Code Analytics - Premium Feature
Sistema de rastreamento de leituras de QR codes para usuários premium.
## 🎯 Visão Geral
Permite que usuários premium acompanhem quantas vezes seus QR codes foram escaneados, com timestamps e histórico completo.
### Como Funciona
1. **Usuário premium** gera QR code com analytics habilitado
2. QR code aponta para URL de redirect: `https://qrrapido.site/t/{trackingId}`
3. Ao escanear, o sistema:
- Incrementa contador de leituras (`scanCount`)
- Atualiza timestamp da última leitura (`lastAccessedAt`)
- Redireciona para URL original (HTTP 302)
---
## 🔧 Implementação Técnica
### Novos Componentes
#### 1. **TrackingController** (`Controllers/TrackingController.cs`)
Endpoints:
- `GET /t/{trackingId}` - Redireciona e contabiliza leitura
- `GET /t/analytics/{trackingId}` - Retorna analytics (requer autenticação)
#### 2. **Campos no MongoDB**
**QRCodeHistory** (campos já existentes, agora em uso):
```csharp
public int ScanCount { get; set; } = 0; // Contador de leituras
public DateTime LastAccessedAt { get; set; } // Última leitura
public bool IsDynamic { get; set; } = false; // Se é trackable
public string? TrackingId { get; set; } // ID público (novo)
```
#### 3. **QRGenerationRequest** (novo campo)
```csharp
public bool EnableTracking { get; set; } = false; // Premium feature
```
#### 4. **UserService** (novos métodos)
```csharp
Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId);
Task IncrementQRScanCountAsync(string trackingId);
```
---
## 🚀 Como Usar
### Backend (já implementado)
#### Gerar QR com Analytics
```csharp
var request = new QRGenerationRequest
{
Type = "url",
Content = "https://google.com",
IsPremium = true,
EnableTracking = true // ✅ Habilita analytics
};
var result = await _qrService.GenerateRapidAsync(request);
// result.TrackingId conterá o ID para analytics
// QR code aponta para: https://qrrapido.site/t/{trackingId}
```
#### Consultar Analytics
```csharp
// Via UserService
var qr = await _userService.GetQRByTrackingIdAsync("abc123def456");
Console.WriteLine($"Leituras: {qr.ScanCount}");
Console.WriteLine($"Última leitura: {qr.LastAccessedAt}");
// Via API (requer autenticação)
GET /t/analytics/abc123def456
```
**Response:**
```json
{
"trackingId": "abc123def456",
"scanCount": 42,
"createdAt": "2025-10-19T10:00:00Z",
"lastAccessedAt": "2025-10-20T15:30:00Z",
"content": "https://google.com",
"type": "url"
}
```
---
### Frontend (ainda não implementado)
#### Opção 1: Checkbox no Gerador
```html
<!-- Em Views/Home/Index.cshtml -->
<div class="form-check premium-feature">
<input type="checkbox" id="enableTracking" class="form-check-input">
<label for="enableTracking">
<i class="fas fa-chart-line"></i> Habilitar Analytics
<span class="badge bg-warning">Premium</span>
</label>
</div>
```
```javascript
// Em wwwroot/js/qr-speed-generator.js
const enableTracking = document.getElementById('enableTracking');
formData.append('EnableTracking', enableTracking?.checked && this.isPremium);
```
#### Opção 2: Dashboard de Analytics
```html
<!-- Nova view: Views/Premium/Analytics.cshtml -->
<div class="analytics-dashboard">
<h2>Meus QR Codes com Analytics</h2>
<div class="qr-list">
@foreach (var qr in Model.TrackableQRs)
{
<div class="qr-card">
<h3>@qr.Content</h3>
<p><strong>Leituras:</strong> @qr.ScanCount</p>
<p><strong>Última leitura:</strong> @qr.LastAccessedAt.ToString("dd/MM/yyyy HH:mm")</p>
<a href="/t/analytics/@qr.TrackingId">Ver detalhes</a>
</div>
}
</div>
</div>
```
---
## 📝 Limitações Conhecidas
### 1. **Apenas QR codes de URL**
- Tracking funciona apenas para `Type = "url"`
- WiFi, vCard, SMS, etc. **não suportam** redirect
**Razão**: Esses tipos não aceitam URLs customizadas
**Solução futura**: Implementar pixel de tracking invisível ou API de scan manual
### 2. **Redirect adiciona ~50-200ms**
- Usuário experimenta pequeno delay ao escanear
- Impacto: Mínimo para maioria dos casos
**Otimização**: Redirect é assíncrono, contador atualiza em background
### 3. **Bloqueadores de ads podem afetar**
- Alguns bloqueadores podem marcar `/t/` como tracking
- Probabilidade: Baixa (não usa cookies ou JS)
---
## 🔐 Segurança
### Proteções Implementadas
1. **Tracking ID não-sequencial**: GUID truncado (12 chars)
2. **Validação de ownership**: Endpoint `/t/analytics/` verifica se QR pertence ao usuário
3. **Fire-and-forget counting**: Não bloqueia redirect se MongoDB estiver lento
4. **Logging completo**: Todas as leituras são logadas
### Considerações de Privacidade
- **Não coleta**: IP, device, localização (por enquanto)
- **Apenas conta**: Quantidade de leituras + timestamp
- **LGPD compliant**: Usuário premium pode deletar histórico a qualquer momento
---
## 📊 Exemplos de Uso
### Caso 1: Rastreamento de Campanha
```csharp
// Gerar QR para campanha de marketing
var campaign = new QRGenerationRequest
{
Type = "url",
Content = "https://minhaloja.com/promo-verao",
IsPremium = true,
EnableTracking = true
};
var qr = await _qrService.GenerateRapidAsync(campaign);
// Distribuir QR em materiais impressos
// Depois consultar: await _userService.GetQRByTrackingIdAsync(qr.TrackingId);
```
### Caso 2: Validação de Engajamento
```csharp
// Verificar se QR code em outdoor teve leituras
var qr = await _userService.GetQRByTrackingIdAsync("abc123def456");
if (qr.ScanCount > 100)
{
Console.WriteLine("Outdoor teve boa visibilidade!");
}
else if (qr.ScanCount == 0)
{
Console.WriteLine("QR pode estar ilegível ou mal posicionado");
}
```
---
## 🛠️ Configuração
### appsettings.json
```json
{
"App": {
"BaseUrl": "https://qrrapido.site" // ✅ Usado para gerar URLs de tracking
}
}
```
### appsettings.Development.json
```json
{
"App": {
"BaseUrl": "https://localhost:52428" // ✅ Para testes locais
}
}
```
---
## 🧪 Testando Localmente
### 1. Gerar QR com Tracking
```bash
# POST /api/QR/GenerateRapid
curl -X POST https://localhost:52428/api/QR/GenerateRapid \
-H "Content-Type: application/json" \
-d '{
"type": "url",
"content": "https://google.com",
"isPremium": true,
"enableTracking": true
}'
```
**Response:**
```json
{
"qrCodeBase64": "iVBORw0KG...",
"trackingId": "a1b2c3d4e5f6",
"success": true
}
```
### 2. Simular Leitura do QR
```bash
# Abrir no navegador (ou curl)
curl -L https://localhost:52428/t/a1b2c3d4e5f6
# Deve redirecionar para https://google.com
```
### 3. Verificar Contador
```bash
# Acessar MongoDB ou via API
curl https://localhost:52428/t/analytics/a1b2c3d4e5f6 \
-H "Authorization: Bearer {token}"
```
**Response:**
```json
{
"trackingId": "a1b2c3d4e5f6",
"scanCount": 1,
"lastAccessedAt": "2025-10-20T10:00:00Z"
}
```
---
## 📈 Próximos Passos (Features Futuras)
### Analytics Avançado
- [ ] Gráfico de leituras por dia/semana/mês
- [ ] Geo-localização (IP → País/Cidade)
- [ ] Tipo de device (mobile/desktop)
- [ ] Browser/OS usado para escanear
### UI/UX
- [ ] Dashboard de analytics no painel premium
- [ ] Exportar dados para CSV/Excel
- [ ] Notificações quando QR atingir X leituras
### Performance
- [ ] Cache Redis para evitar queries MongoDB em cada scan
- [ ] Batch updates (atualizar contador a cada N leituras)
---
## 📚 Referências
- **Tracking URLs**: Mesma estratégia usada por bit.ly, tinyurl, etc.
- **HTTP 302 Redirect**: Padrão para preservar SEO e funcionalidade
- **Fire-and-forget**: Pattern do ASP.NET Core para operações assíncronas não-bloqueantes
---
## 🐛 Troubleshooting
### Problema: "QR code not found"
- Verificar se `trackingId` existe no MongoDB
- Verificar se QR foi salvo com `IsDynamic = true`
### Problema: Contador não incrementa
- Verificar logs do `TrackingController`
- Verificar conectividade MongoDB
- Verificar se `IncrementQRScanCountAsync` está sendo chamado
### Problema: Redirect não funciona
- Verificar se URL original está salva em `Content`
- Verificar se `BaseUrl` está configurado corretamente
- Verificar logs de erro no controller
---
**Implementado em**: 2025-10-20
**Versão**: 1.0.0
**Status**: ✅ Backend completo, Frontend pendente

View File

@ -36,9 +36,6 @@ namespace QRRapidoApp.Models
[BsonElement("isDynamic")] [BsonElement("isDynamic")]
public bool IsDynamic { get; set; } = false; public bool IsDynamic { get; set; } = false;
[BsonElement("trackingId")]
public string? TrackingId { get; set; } // Public ID for tracking URL (premium feature)
[BsonElement("size")] [BsonElement("size")]
public int Size { get; set; } = 300; public int Size { get; set; } = 300;

View File

@ -28,12 +28,6 @@ namespace QRRapidoApp.Models.ViewModels
/// Se deve aplicar a cor do QR code no logo (Premium feature) /// Se deve aplicar a cor do QR code no logo (Premium feature)
/// </summary> /// </summary>
public bool ApplyLogoColorization { get; set; } = false; public bool ApplyLogoColorization { get; set; } = false;
/// <summary>
/// Se deve habilitar analytics de leitura (Premium feature)
/// Quando habilitado, o QR code usa URL de redirect para contabilizar leituras
/// </summary>
public bool EnableTracking { get; set; } = false;
} }
public class QRGenerationResult public class QRGenerationResult
@ -48,6 +42,5 @@ namespace QRRapidoApp.Models.ViewModels
public bool Success { get; set; } = true; public bool Success { get; set; } = true;
public string? ErrorMessage { get; set; } public string? ErrorMessage { get; set; }
public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade
public string? TrackingId { get; set; } // Tracking ID for analytics (Premium feature)
} }
} }

View File

@ -783,23 +783,4 @@
<data name="AnonymousUserLimit" xml:space="preserve"> <data name="AnonymousUserLimit" xml:space="preserve">
<value>Anonymous users: 3 QR codes per day</value> <value>Anonymous users: 3 QR codes per day</value>
</data> </data>
<!-- Premium Features -->
<data name="AdvancedCustomization" xml:space="preserve">
<value>Advanced customization (colors, corners)</value>
</data>
<data name="LogoSupport" xml:space="preserve">
<value>Logo and image support</value>
</data>
<data name="HistoryAndDownloads" xml:space="preserve">
<value>Unlimited history and downloads</value>
</data>
<data name="QRReadCounter" xml:space="preserve">
<value>QR code scan counter</value>
</data>
<data name="EnableTracking" xml:space="preserve">
<value>Enable scan counter</value>
</data>
<data name="TrackingInfo" xml:space="preserve">
<value>QR code will use redirect URL to count scans</value>
</data>
</root> </root>

View File

@ -2041,15 +2041,6 @@
<data name="HistoryAndDownloads" xml:space="preserve"> <data name="HistoryAndDownloads" xml:space="preserve">
<value>Historial y descargas ilimitadas</value> <value>Historial y descargas ilimitadas</value>
</data> </data>
<data name="QRReadCounter" xml:space="preserve">
<value>Contador de lecturas de códigos QR</value>
</data>
<data name="EnableTracking" xml:space="preserve">
<value>Habilitar contador de lecturas</value>
</data>
<data name="TrackingInfo" xml:space="preserve">
<value>El código QR usará URL de redireccionamiento para contabilizar lecturas</value>
</data>
<!-- FAQ Updates --> <!-- FAQ Updates -->
<data name="FAQ_StaticQRExplanation" xml:space="preserve"> <data name="FAQ_StaticQRExplanation" xml:space="preserve">
<value>Todos los códigos QR generados por QR Rápido son estáticos y permanentes. Esto significa que el contenido está codificado directamente en el código QR y no puede ser modificado después de la generación. Este enfoque garantiza máxima privacidad, ya que no rastreamos escaneos, y funciona sin conexión sin depender de servidores externos.</value> <value>Todos los códigos QR generados por QR Rápido son estáticos y permanentes. Esto significa que el contenido está codificado directamente en el código QR y no puede ser modificado después de la generación. Este enfoque garantiza máxima privacidad, ya que no rastreamos escaneos, y funciona sin conexión sin depender de servidores externos.</value>

View File

@ -2131,15 +2131,6 @@
<data name="HistoryAndDownloads" xml:space="preserve"> <data name="HistoryAndDownloads" xml:space="preserve">
<value>Histórico e downloads ilimitados</value> <value>Histórico e downloads ilimitados</value>
</data> </data>
<data name="QRReadCounter" xml:space="preserve">
<value>Contador de leituras de QR codes</value>
</data>
<data name="EnableTracking" xml:space="preserve">
<value>Habilitar contador de leituras</value>
</data>
<data name="TrackingInfo" xml:space="preserve">
<value>QR code usará URL de redirect para contabilizar leituras</value>
</data>
<!-- FAQ Updates --> <!-- FAQ Updates -->
<data name="FAQ_StaticQRExplanation" xml:space="preserve"> <data name="FAQ_StaticQRExplanation" xml:space="preserve">
<value>Todos os QR codes gerados pelo QR Rápido são estáticos e permanentes. Isso significa que o conteúdo é codificado diretamente no QR code e não pode ser alterado após a geração. Essa abordagem garante máxima privacidade, pois não rastreamos scans, e funciona offline sem depender de servidores externos.</value> <value>Todos os QR codes gerados pelo QR Rápido são estáticos e permanentes. Isso significa que o conteúdo é codificado diretamente no QR code e não pode ser alterado após a geração. Essa abordagem garante máxima privacidade, pois não rastreamos scans, e funciona offline sem depender de servidores externos.</value>

View File

@ -28,9 +28,5 @@ namespace QRRapidoApp.Services
Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt); Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt);
Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate); Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate);
Task DeleteUserHistoryAsync(string userId); Task DeleteUserHistoryAsync(string userId);
// QR Code Tracking (Analytics) - Premium feature
Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId);
Task IncrementQRScanCountAsync(string trackingId);
} }
} }

View File

@ -45,30 +45,6 @@ namespace QRRapidoApp.Services
{ {
await _semaphore.WaitAsync(); await _semaphore.WaitAsync();
// Generate tracking ID for premium users with tracking enabled
string? trackingId = null;
string originalContent = request.Content;
// DEBUG: Log all conditions
_logger.LogInformation("[TRACKING DEBUG] IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}",
request.IsPremium, request.EnableTracking, request.Type);
if (request.IsPremium && request.EnableTracking && request.Type == "url")
{
// Only URL type QR codes can use tracking (redirect functionality)
trackingId = Guid.NewGuid().ToString("N").Substring(0, 12); // 12-char tracking ID
var baseUrl = _config.GetValue<string>("App:BaseUrl", "https://qrrapido.site");
request.Content = $"{baseUrl}/t/{trackingId}";
_logger.LogInformation("✅ Tracking enabled for QR - TrackingId: {TrackingId}, OriginalURL: {OriginalURL}",
trackingId, originalContent);
}
else
{
_logger.LogWarning("❌ Tracking NOT enabled - IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}",
request.IsPremium, request.EnableTracking, request.Type);
}
// Cache key based on content and settings // Cache key based on content and settings
var cacheKey = GenerateCacheKey(request); var cacheKey = GenerateCacheKey(request);
var cached = await _cache.GetStringAsync(cacheKey); var cached = await _cache.GetStringAsync(cacheKey);
@ -83,13 +59,13 @@ namespace QRRapidoApp.Services
{ {
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds; cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
cachedResult.FromCache = true; cachedResult.FromCache = true;
// Se não tem análise de legibilidade no cache, gerar agora // Se não tem análise de legibilidade no cache, gerar agora
if (cachedResult.ReadabilityInfo == null) if (cachedResult.ReadabilityInfo == null)
{ {
cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size); cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size);
} }
return cachedResult; return cachedResult;
} }
} }
@ -110,16 +86,9 @@ namespace QRRapidoApp.Services
Size = qrCode.Length, Size = qrCode.Length,
RequestSettings = request, RequestSettings = request,
Success = true, Success = true,
ReadabilityInfo = readabilityInfo, ReadabilityInfo = readabilityInfo
TrackingId = trackingId // Include tracking ID if enabled
}; };
// Restore original content if tracking was enabled
if (trackingId != null)
{
request.Content = originalContent;
}
// Cache for configurable time // Cache for configurable time
var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60)); var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60));
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
@ -679,230 +648,21 @@ namespace QRRapidoApp.Services
private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize) private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize)
{ {
if (cornerStyle == "square" || string.IsNullOrEmpty(cornerStyle))
{
return qrBytes; // No processing needed for square style
}
try try
{ {
_logger.LogInformation("Applying corner style '{CornerStyle}' to QR code", cornerStyle); // Simplified implementation for cross-platform compatibility
// The complex corner styling can be re-implemented later using ImageSharp drawing primitives
using var qrImage = Image.Load<Rgba32>(qrBytes); _logger.LogInformation("Corner style '{CornerStyle}' temporarily disabled for cross-platform compatibility. Returning original QR code.", cornerStyle);
int width = qrImage.Width; return qrBytes;
int height = qrImage.Height;
// Detect module size by scanning the image
int moduleSize = DetectModuleSize(qrImage);
if (moduleSize <= 0)
{
_logger.LogWarning("Could not detect module size, returning original QR code");
return qrBytes;
}
_logger.LogDebug("Detected module size: {ModuleSize}px for {Width}x{Height} QR code", moduleSize, width, height);
// Create a new image with styled modules
using var styledImage = new Image<Rgba32>(width, height);
// Process each module
for (int y = 0; y < height; y += moduleSize)
{
for (int x = 0; x < width; x += moduleSize)
{
// Check if this module is dark (part of the QR pattern)
var centerPixel = qrImage[x + moduleSize / 2, y + moduleSize / 2];
bool isDark = centerPixel.R < 128; // Dark if RGB < 128
if (isDark)
{
// Draw styled module based on cornerStyle
DrawStyledModule(styledImage, x, y, moduleSize, cornerStyle, centerPixel);
}
else
{
// Draw background (light modules)
DrawRectangle(styledImage, x, y, moduleSize, moduleSize, centerPixel);
}
}
}
// Convert back to bytes
using var outputStream = new MemoryStream();
styledImage.SaveAsPng(outputStream);
_logger.LogInformation("Successfully applied corner style '{CornerStyle}'", cornerStyle);
return outputStream.ToArray();
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error applying corner style {CornerStyle}, returning original QR code", cornerStyle); _logger.LogError(ex, "Error applying corner style {CornerStyle}, returning original QR code", cornerStyle);
// Return original QR code if styling fails
return qrBytes; return qrBytes;
} }
} }
private int DetectModuleSize(Image<Rgba32> qrImage)
{
// Scan horizontally from left to find the first transition from light to dark
int width = qrImage.Width;
int height = qrImage.Height;
int midY = height / 2;
bool wasLight = true;
int darkStart = -1;
int darkEnd = -1;
for (int x = 0; x < width; x++)
{
var pixel = qrImage[x, midY];
bool isLight = pixel.R >= 128;
if (wasLight && !isLight)
{
// Transition from light to dark
darkStart = x;
}
else if (!wasLight && isLight && darkStart >= 0)
{
// Transition from dark to light
darkEnd = x;
break;
}
wasLight = isLight;
}
if (darkStart >= 0 && darkEnd > darkStart)
{
return darkEnd - darkStart;
}
// Fallback: estimate based on typical QR code structure (21-177 modules)
// Most common is 25-29 modules for QR version 1-5
return width / 25; // Rough estimate
}
private void DrawStyledModule(Image<Rgba32> image, int x, int y, int size, string style, Rgba32 color)
{
switch (style.ToLower())
{
case "rounded":
DrawRoundedRectangle(image, x, y, size, size, size / 4, color);
break;
case "circle":
DrawCircle(image, x + size / 2, y + size / 2, size / 2, color);
break;
default:
DrawRectangle(image, x, y, size, size, color);
break;
}
}
private void DrawRectangle(Image<Rgba32> image, int x, int y, int width, int height, Rgba32 color)
{
for (int dy = 0; dy < height; dy++)
{
for (int dx = 0; dx < width; dx++)
{
int px = x + dx;
int py = y + dy;
if (px >= 0 && px < image.Width && py >= 0 && py < image.Height)
{
image[px, py] = color;
}
}
}
}
private void DrawRoundedRectangle(Image<Rgba32> image, int x, int y, int width, int height, int radius, Rgba32 color)
{
for (int dy = 0; dy < height; dy++)
{
for (int dx = 0; dx < width; dx++)
{
int px = x + dx;
int py = y + dy;
if (px < 0 || px >= image.Width || py < 0 || py >= image.Height)
continue;
// Check if pixel is inside rounded rectangle
bool inCorner = false;
int distX = 0, distY = 0;
// Top-left corner
if (dx < radius && dy < radius)
{
distX = radius - dx;
distY = radius - dy;
inCorner = true;
}
// Top-right corner
else if (dx >= width - radius && dy < radius)
{
distX = dx - (width - radius - 1);
distY = radius - dy;
inCorner = true;
}
// Bottom-left corner
else if (dx < radius && dy >= height - radius)
{
distX = radius - dx;
distY = dy - (height - radius - 1);
inCorner = true;
}
// Bottom-right corner
else if (dx >= width - radius && dy >= height - radius)
{
distX = dx - (width - radius - 1);
distY = dy - (height - radius - 1);
inCorner = true;
}
if (inCorner)
{
// Check if point is inside the rounded corner
double distance = Math.Sqrt(distX * distX + distY * distY);
if (distance <= radius)
{
image[px, py] = color;
}
}
else
{
// Inside the main rectangle (not in corners)
image[px, py] = color;
}
}
}
}
private void DrawCircle(Image<Rgba32> image, int centerX, int centerY, int radius, Rgba32 color)
{
int x0 = centerX - radius;
int y0 = centerY - radius;
int diameter = radius * 2;
for (int dy = 0; dy <= diameter; dy++)
{
for (int dx = 0; dx <= diameter; dx++)
{
int px = x0 + dx;
int py = y0 + dy;
if (px < 0 || px >= image.Width || py < 0 || py >= image.Height)
continue;
// Calculate distance from center
double distance = Math.Sqrt(Math.Pow(dx - radius, 2) + Math.Pow(dy - radius, 2));
if (distance <= radius)
{
image[px, py] = color;
}
}
}
}
// QRModule class removed - was only used for corner styling which is temporarily simplified // QRModule class removed - was only used for corner styling which is temporarily simplified
} }
} }

View File

@ -231,9 +231,7 @@ namespace QRRapidoApp.Services
GenerationTimeMs = qrResult.GenerationTimeMs, GenerationTimeMs = qrResult.GenerationTimeMs,
FromCache = qrResult.FromCache, FromCache = qrResult.FromCache,
IsActive = true, IsActive = true,
LastAccessedAt = DateTime.UtcNow, LastAccessedAt = DateTime.UtcNow
TrackingId = qrResult.TrackingId, // Save tracking ID for analytics
IsDynamic = !string.IsNullOrEmpty(qrResult.TrackingId) // Mark as dynamic if tracking is enabled
}; };
await _context.QRCodeHistory.InsertOneAsync(qrHistory); await _context.QRCodeHistory.InsertOneAsync(qrHistory);
@ -450,52 +448,5 @@ namespace QRRapidoApp.Services
var update = Builders<User>.Update.Set(u => u.StripeCustomerId, stripeCustomerId); var update = Builders<User>.Update.Set(u => u.StripeCustomerId, stripeCustomerId);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update); await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
} }
/// <summary>
/// Get QR code by tracking ID (for analytics)
/// </summary>
public async Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId)
{
try
{
return await _context.QRCodeHistory
.Find(q => q.TrackingId == trackingId && q.IsActive)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting QR by tracking ID {TrackingId}: {ErrorMessage}", trackingId, ex.Message);
return null;
}
}
/// <summary>
/// Increment scan count for QR code analytics
/// </summary>
public async Task IncrementQRScanCountAsync(string trackingId)
{
try
{
var filter = Builders<QRCodeHistory>.Filter.Eq(q => q.TrackingId, trackingId);
var update = Builders<QRCodeHistory>.Update
.Inc(q => q.ScanCount, 1)
.Set(q => q.LastAccessedAt, DateTime.UtcNow);
var result = await _context.QRCodeHistory.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0)
{
_logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}", trackingId);
}
else
{
_logger.LogWarning("Failed to increment scan count - TrackingId not found: {TrackingId}", trackingId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error incrementing scan count for {TrackingId}: {ErrorMessage}", trackingId, ex.Message);
}
}
} }
} }

View File

@ -682,7 +682,7 @@
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="premium-upgrade-box p-3 border rounded bg-light"> <div class="premium-upgrade-box p-3 border rounded bg-light">
<h6 class="fw-bold mb-2"> <h6 class="fw-bold mb-2">
<i class="fas fa-crown text-warning"></i> <i class="fas fa-crown text-warning"></i>
Logo Personalizado - Premium Logo Personalizado - Premium
</h6> </h6>
<p class="mb-2 small">Adicione sua marca aos QR Codes! Agora com tamanho configurável (10-25%) e colorização automática.</p> <p class="mb-2 small">Adicione sua marca aos QR Codes! Agora com tamanho configurável (10-25%) e colorização automática.</p>
@ -692,23 +692,6 @@
</div> </div>
</div> </div>
} }
@* Analytics/Tracking - Premium Feature *@
@if (isPremium)
{
<div class="col-12 mb-3">
<div class="form-check">
<input type="checkbox" id="enable-tracking" class="form-check-input" value="true">
<label class="form-check-label" for="enable-tracking">
<i class="fas fa-chart-line text-primary"></i> @Localizer["EnableTracking"]
<span class="badge bg-primary">Premium</span>
</label>
<div class="form-text">
<i class="fas fa-info-circle"></i> @Localizer["TrackingInfo"]
</div>
</div>
</div>
}
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">@Localizer["BorderStyle"]</label> <label class="form-label">@Localizer["BorderStyle"]</label>
@ -718,11 +701,13 @@
{ {
<option value="rounded">@Localizer["Rounded"] 👑</option> <option value="rounded">@Localizer["Rounded"] 👑</option>
<option value="circle">Círculos 👑</option> <option value="circle">Círculos 👑</option>
<option value="leaf">Folha 👑</option>
} }
else else
{ {
<option value="rounded" disabled>@Localizer["Rounded"] - Premium 👑</option> <option value="rounded" disabled>@Localizer["Rounded"] - Premium 👑</option>
<option value="circle" disabled>Círculos - Premium 👑</option> <option value="circle" disabled>Círculos - Premium 👑</option>
<option value="leaf" disabled>Folha - Premium 👑</option>
} }
</select> </select>
@if (!isPremium) @if (!isPremium)
@ -1210,7 +1195,6 @@
<li><i class="fas fa-check text-success"></i> @Localizer["AdvancedCustomization"]</li> <li><i class="fas fa-check text-success"></i> @Localizer["AdvancedCustomization"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["LogoSupport"]</li> <li><i class="fas fa-check text-success"></i> @Localizer["LogoSupport"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["HistoryAndDownloads"]</li> <li><i class="fas fa-check text-success"></i> @Localizer["HistoryAndDownloads"]</li>
<li><i class="fas fa-chart-line text-success"></i> @Localizer["QRReadCounter"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["PrioritySupport"]</li> <li><i class="fas fa-check text-success"></i> @Localizer["PrioritySupport"]</li>
</ul> </ul>
<div class="text-center"> <div class="text-center">

View File

@ -76,7 +76,6 @@
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["AdvancedCustomization"]</li> <li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["AdvancedCustomization"]</li>
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["LogoSupport"]</li> <li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["LogoSupport"]</li>
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["HistoryAndDownloads"]</li> <li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["HistoryAndDownloads"]</li>
<li class="list-group-item border-0"><i class="fas fa-chart-line text-success me-2"></i>@Localizer["QRReadCounter"]</li>
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["PrioritySupport"]</li> <li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["PrioritySupport"]</li>
</ul> </ul>
</div> </div>

8
package-lock.json generated
View File

@ -8,7 +8,7 @@
"name": "qrrapido-app", "name": "qrrapido-app",
"version": "1.0.0", "version": "1.0.0",
"devDependencies": { "devDependencies": {
"vite": "^5.4.21" "vite": "^5.4.0"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
@ -879,9 +879,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.21", "version": "5.4.20",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -8,6 +8,6 @@
"preview": "vite preview --config vite.config.js" "preview": "vite preview --config vite.config.js"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.4.21" "vite": "^5.4.0"
} }
} }

View File

@ -1,14 +1,9 @@
// QR Rapido Speed Generator // QR Rapido Speed Generator
class QRRapidoGenerator { class QRRapidoGenerator {
constructor() { constructor() {
this.startTime = 0; this.startTime = 0;
this.currentQR = null; this.currentQR = null;
this.timerInterval = null; this.timerInterval = null;
// Initialize premium status from hidden input
this.isPremium = this.checkUserPremium();
console.log('[INIT] QRRapidoGenerator - isPremium:', this.isPremium);
this.languageStrings = { this.languageStrings = {
'pt-BR': { 'pt-BR': {
tagline: 'Gere QR codes em segundos!', tagline: 'Gere QR codes em segundos!',
@ -742,9 +737,6 @@ class QRRapidoGenerator {
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF'); const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
// Common data for both endpoints // Common data for both endpoints
const trackingEnabled = this.getTrackingEnabled();
console.log('[DEBUG] getTrackingEnabled() returned:', trackingEnabled, 'type:', typeof trackingEnabled);
const commonData = { const commonData = {
type: actualType, type: actualType,
content: encodedContent, content: encodedContent,
@ -755,12 +747,9 @@ class QRRapidoGenerator {
margin: parseInt(document.getElementById('qr-margin').value), margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square', cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true, optimizeForSpeed: true,
language: this.currentLang, language: this.currentLang
enableTracking: trackingEnabled // Analytics feature for premium users
}; };
console.log('[DEBUG] commonData.enableTracking:', commonData.enableTracking);
// Add dynamic QR data if it's a URL type // Add dynamic QR data if it's a URL type
if (type === 'url') { if (type === 'url') {
let dynamicData; let dynamicData;
@ -783,15 +772,11 @@ class QRRapidoGenerator {
// Use FormData for requests with logo // Use FormData for requests with logo
const formData = new FormData(); const formData = new FormData();
// Add all common data to FormData with PascalCase for ASP.NET Core model binding // Add all common data to FormData
Object.keys(commonData).forEach(key => { Object.keys(commonData).forEach(key => {
// Convert camelCase to PascalCase for FormData (ASP.NET Core compatibility) formData.append(key, commonData[key]);
const pascalKey = key.charAt(0).toUpperCase() + key.slice(1);
const value = commonData[key];
console.log('[DEBUG] FormData.append:', pascalKey, '=', value, 'type:', typeof value);
formData.append(pascalKey, value);
}); });
// NOVOS PARÂMETROS DE LOGO APRIMORADO // NOVOS PARÂMETROS DE LOGO APRIMORADO
const logoSettings = this.getLogoSettings(); const logoSettings = this.getLogoSettings();
formData.append('LogoSizePercent', logoSettings.logoSizePercent.toString()); formData.append('LogoSizePercent', logoSettings.logoSizePercent.toString());
@ -823,38 +808,13 @@ class QRRapidoGenerator {
getLogoSettings() { getLogoSettings() {
const logoSizeSlider = document.getElementById('logo-size-slider'); const logoSizeSlider = document.getElementById('logo-size-slider');
const logoColorizeToggle = document.getElementById('logo-colorize-toggle'); const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
return { return {
logoSizePercent: parseInt(logoSizeSlider?.value || '20'), logoSizePercent: parseInt(logoSizeSlider?.value || '20'),
applyColorization: logoColorizeToggle?.checked || false applyColorization: logoColorizeToggle?.checked || false
}; };
} }
// Function to check if tracking is enabled (Premium feature)
getTrackingEnabled() {
const trackingCheckbox = document.getElementById('enable-tracking');
// DEBUG: Log each condition separately
console.log('[DEBUG] getTrackingEnabled() - Checkbox exists:', !!trackingCheckbox);
console.log('[DEBUG] getTrackingEnabled() - Checkbox checked:', trackingCheckbox?.checked);
console.log('[DEBUG] getTrackingEnabled() - this.isPremium:', this.isPremium);
// Only return true if checkbox exists, is checked, and user is premium
// IMPORTANT: Always return boolean (not undefined) for FormData compatibility
const result = !!(trackingCheckbox && trackingCheckbox.checked && this.isPremium);
console.log('[DEBUG] getTrackingEnabled() - Final result:', result);
return result;
}
// Check if user has premium status
checkUserPremium() {
const premiumStatus = document.getElementById('user-premium-status');
if (premiumStatus) {
return premiumStatus.value === 'premium';
}
return false;
}
// Função auxiliar para obter conteúdo baseado no tipo // Função auxiliar para obter conteúdo baseado no tipo
getContentForType(type) { getContentForType(type) {
if (type === 'url') { if (type === 'url') {
@ -1182,8 +1142,8 @@ class QRRapidoGenerator {
handleCornerStyleChange(e) { handleCornerStyleChange(e) {
const selectedStyle = e.target.value; const selectedStyle = e.target.value;
const premiumStyles = ['rounded', 'circle']; const premiumStyles = ['rounded', 'circle', 'leaf'];
if (premiumStyles.includes(selectedStyle)) { if (premiumStyles.includes(selectedStyle)) {
// Check if user is premium (we can detect this by checking if the option is disabled) // Check if user is premium (we can detect this by checking if the option is disabled)
const option = e.target.options[e.target.selectedIndex]; const option = e.target.options[e.target.selectedIndex];
@ -1194,12 +1154,6 @@ class QRRapidoGenerator {
return; return;
} }
} }
// If a QR code already exists, regenerate with new corner style
if (this.currentQR) {
console.log('[CORNER STYLE] Corner style changed to:', selectedStyle, '- regenerating QR code');
this.generateQRWithTimer(e);
}
} }
applyQuickStyle(e) { applyQuickStyle(e) {