From 04f406f6bccf7dbd949105b9e1e2ca215883f243 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sat, 13 Sep 2025 23:32:09 -0300 Subject: [PATCH] fix: preview token --- src/BCards.Web/Controllers/AdminController.cs | 13 +- .../Middleware/PageStatusMiddleware.cs | 4 + .../Middleware/PreviewTokenMiddleware.cs | 121 ------------------ .../Repositories/UserPageRepository.cs | 10 +- src/BCards.Web/Services/ModerationService.cs | 18 ++- src/BCards.Web/Views/Admin/Dashboard.cshtml | 64 ++++++++- 6 files changed, 97 insertions(+), 133 deletions(-) delete mode 100644 src/BCards.Web/Middleware/PreviewTokenMiddleware.cs diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index 2f79a99..0bc48d1 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -962,9 +962,10 @@ public class AdminController : Controller if (pageItem == null || pageItem.UserId != user.Id) return Json(new { success = false, message = "Página não encontrada" }); - // Só renovar token para páginas "Creating" - if (pageItem.Status != ViewModels.PageStatus.Creating) - return Json(new { success = false, message = "Token só pode ser renovado para páginas em desenvolvimento" }); + // Só renovar token para páginas "Creating" e "Rejected" + if (pageItem.Status != ViewModels.PageStatus.Creating && + pageItem.Status != ViewModels.PageStatus.Rejected) + return Json(new { success = false, message = "Token só pode ser renovado para páginas em desenvolvimento ou rejeitadas" }); try { @@ -973,11 +974,11 @@ public class AdminController : Controller var previewUrl = $"{Request.Scheme}://{Request.Host}/page/{pageItem.Category}/{pageItem.Slug}?preview={newToken}"; - return Json(new { - success = true, + return Json(new { + success = true, previewToken = newToken, previewUrl = previewUrl, - expiresAt = DateTime.UtcNow.AddHours(4).ToString("yyyy-MM-dd HH:mm:ss") + expiresAt = DateTime.UtcNow.AddMinutes(5).ToString("yyyy-MM-dd HH:mm:ss") }); } catch (Exception ex) diff --git a/src/BCards.Web/Middleware/PageStatusMiddleware.cs b/src/BCards.Web/Middleware/PageStatusMiddleware.cs index a9ea8c5..0d3f844 100644 --- a/src/BCards.Web/Middleware/PageStatusMiddleware.cs +++ b/src/BCards.Web/Middleware/PageStatusMiddleware.cs @@ -81,6 +81,10 @@ public class PageStatusMiddleware return; } + // LOG DETALHADO ANTES da comparação + _logger.LogInformation("Token comparison for page {PageId} - Provided: {ProvidedToken}, DB Token: {DbToken}, DB Expiry: {DbExpiry}", + page.Id, previewToken, page.PreviewToken, page.PreviewTokenExpiry); + if (previewToken != page.PreviewToken) { _logger.LogInformation($"User id: {userId} - Page {category}/{slug} preview token mismatch - provided: {previewToken}, expected: {page.PreviewToken}"); diff --git a/src/BCards.Web/Middleware/PreviewTokenMiddleware.cs b/src/BCards.Web/Middleware/PreviewTokenMiddleware.cs deleted file mode 100644 index 2a90506..0000000 --- a/src/BCards.Web/Middleware/PreviewTokenMiddleware.cs +++ /dev/null @@ -1,121 +0,0 @@ -using BCards.Web.Services; -using Microsoft.Extensions.Caching.Memory; - -namespace BCards.Web.Middleware; - -public class PreviewTokenMiddleware -{ - private readonly RequestDelegate _next; - private readonly IMemoryCache _cache; - private readonly ILogger _logger; - - public PreviewTokenMiddleware(RequestDelegate next, IMemoryCache cache, ILogger logger) - { - _next = next; - _cache = cache; - _logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - var path = context.Request.Path.Value; - var query = context.Request.Query; - - // Verificar se é uma requisição de preview - if (path != null && path.StartsWith("/page/") && query.ContainsKey("preview")) - { - var previewToken = query["preview"].FirstOrDefault(); - - if (!string.IsNullOrEmpty(previewToken)) - { - var result = await HandlePreviewRequest(context, previewToken); - if (!result) - { - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Preview não encontrado ou expirado."); - return; - } - } - } - - await _next(context); - } - - private async Task HandlePreviewRequest(HttpContext context, string previewToken) - { - try - { - // Verificar rate limiting por IP - var clientIp = GetClientIpAddress(context); - var rateLimitKey = $"preview_rate_limit_{clientIp}"; - - if (_cache.TryGetValue(rateLimitKey, out int requestCount)) - { - if (requestCount >= 10) // Máximo 10 requisições por minuto por IP - { - _logger.LogWarning("Rate limit exceeded for IP {IP} on preview token {Token}", clientIp, previewToken); - return false; - } - _cache.Set(rateLimitKey, requestCount + 1, TimeSpan.FromMinutes(1)); - } - else - { - _cache.Set(rateLimitKey, 1, TimeSpan.FromMinutes(1)); - } - - // Verificar se o token é válido - var moderationService = context.RequestServices.GetService(); - if (moderationService == null) - { - _logger.LogError("ModerationService not found in DI container"); - return false; - } - - var page = await moderationService.GetPageByPreviewTokenAsync(previewToken); - if (page == null) - { - _logger.LogInformation("Invalid or expired preview token: {Token}", previewToken); - return false; - } - - // Incrementar contador de visualizações - var incrementResult = await moderationService.IncrementPreviewViewAsync(page.Id); - if (!incrementResult) - { - _logger.LogWarning("Preview view limit exceeded for page {PageId}", page.Id); - return false; - } - - // Adicionar informações do preview ao contexto - context.Items["IsPreview"] = true; - context.Items["PreviewPageId"] = page.Id; - context.Items["PreviewToken"] = previewToken; - - _logger.LogInformation("Valid preview request for page {PageId} with token {Token}", page.Id, previewToken); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling preview request with token {Token}", previewToken); - return false; - } - } - - private string GetClientIpAddress(HttpContext context) - { - // Verificar cabeçalhos de proxy - var xForwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); - if (!string.IsNullOrEmpty(xForwardedFor)) - { - return xForwardedFor.Split(',')[0].Trim(); - } - - var xRealIp = context.Request.Headers["X-Real-IP"].FirstOrDefault(); - if (!string.IsNullOrEmpty(xRealIp)) - { - return xRealIp; - } - - return context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; - } -} \ No newline at end of file diff --git a/src/BCards.Web/Repositories/UserPageRepository.cs b/src/BCards.Web/Repositories/UserPageRepository.cs index cfdf40b..708687a 100644 --- a/src/BCards.Web/Repositories/UserPageRepository.cs +++ b/src/BCards.Web/Repositories/UserPageRepository.cs @@ -30,7 +30,9 @@ public class UserPageRepository : IUserPageRepository public async Task GetByIdAsync(string id) { - return await _pages.Find(x => x.Id == id && x.IsActive).FirstOrDefaultAsync(); + // Forçar leitura do primary para consultas críticas (preview tokens) + return await _pages.WithReadPreference(ReadPreference.Primary) + .Find(x => x.Id == id && x.IsActive).FirstOrDefaultAsync(); } public async Task GetBySlugAsync(string category, string slug) @@ -47,8 +49,10 @@ public class UserPageRepository : IUserPageRepository { Collation = collation }; - - var cursor = await _pages.FindAsync(filter, findOptions); + + // Forçar leitura do primary para consultas críticas (preview tokens) + var cursor = await _pages.WithReadPreference(ReadPreference.Primary) + .FindAsync(filter, findOptions); return await cursor.FirstOrDefaultAsync(); } diff --git a/src/BCards.Web/Services/ModerationService.cs b/src/BCards.Web/Services/ModerationService.cs index 754fc2b..31c4c29 100644 --- a/src/BCards.Web/Services/ModerationService.cs +++ b/src/BCards.Web/Services/ModerationService.cs @@ -27,15 +27,29 @@ public class ModerationService : IModerationService public async Task GeneratePreviewTokenAsync(string pageId) { var token = Guid.NewGuid().ToString("N")[..16]; - var expiry = DateTime.UtcNow.AddHours(4); // Token válido por 4 horas + var expiry = DateTime.UtcNow.AddMinutes(5); // Token válido por 5 minutos + + // LOG ANTES da busca + _logger.LogInformation("Generating token for page {PageId} - searching page in DB", pageId); + var page = await _userPageRepository.GetByIdAsync(pageId); + + // LOG TOKEN ANTERIOR + _logger.LogInformation("Page {PageId} - Old token: {OldToken}, New token: {NewToken}", + pageId, page.PreviewToken, token); + page.PreviewToken = token; page.PreviewTokenExpiry = expiry; page.PreviewViewCount = 0; + // LOG ANTES do update + _logger.LogInformation("Updating page {PageId} with new token {Token}", pageId, token); + await _userPageRepository.UpdateAsync(page); - _logger.LogInformation("Generated preview token for page {PageId}", pageId); + // LOG APÓS update + _logger.LogInformation("Successfully updated page {PageId} with token {Token}", pageId, token); + return token; } diff --git a/src/BCards.Web/Views/Admin/Dashboard.cshtml b/src/BCards.Web/Views/Admin/Dashboard.cshtml index 2d8c8b3..dd15285 100644 --- a/src/BCards.Web/Views/Admin/Dashboard.cshtml +++ b/src/BCards.Web/Views/Admin/Dashboard.cshtml @@ -374,6 +374,52 @@ } }); + // Sistema de auto-refresh para tokens de preview + let refreshInterval; + let activePreviewPages = new Map(); // pageId -> {category, slug, windowRef} + + // Iniciar auto-refresh a cada 4 minutos + function startAutoRefresh() { + if (refreshInterval) clearInterval(refreshInterval); + refreshInterval = setInterval(async () => { + if (activePreviewPages.size > 0) { + console.log(`Auto-refreshing ${activePreviewPages.size} active preview tokens...`); + for (const [pageId, pageData] of activePreviewPages) { + if (!pageData.windowRef.closed) { + await refreshPageToken(pageId, pageData); + } else { + // Aba fechada, remover do tracking + activePreviewPages.delete(pageId); + } + } + } + }, 4 * 60 * 1000); // 4 minutos + } + + async function refreshPageToken(pageId, pageData) { + try { + // Usar o endpoint específico para refresh ao invés de generate + const response = await fetch(`/Admin/RefreshPreviewToken/${pageId}`, { + method: 'POST', + headers: { 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value } + }); + const result = await response.json(); + if (result.success) { + // Atualizar URL da aba existente + const newUrl = `${window.location.origin}/page/${pageData.category}/${pageData.slug}?preview=${result.previewToken}`; + pageData.windowRef.location.href = newUrl; + console.log(`Token refreshed for page ${pageId}: ${result.previewToken}`); + } else { + console.warn(`Failed to refresh token for page ${pageId}: ${result.message}`); + } + } catch (error) { + console.error(`Failed to refresh token for page ${pageId}:`, error); + } + } + + // Iniciar auto-refresh quando a página carrega + startAutoRefresh(); + // Funções existentes (submitForModeration, openPreview, etc.) async function openPreview(pageId) { const button = event.target.closest('button'); @@ -388,7 +434,23 @@ }); const result = await response.json(); if (result.success) { - window.open(`${window.location.origin}/page/${category}/${slug}?preview=${result.previewToken}`, '_blank'); + // Delay de 500ms para garantir commit no MongoDB + await new Promise(resolve => setTimeout(resolve, 500)); + + // Abrir nova aba e adicionar ao tracking + const previewWindow = window.open( + `${window.location.origin}/page/${category}/${slug}?preview=${result.previewToken}`, + `preview_${pageId}` // Nome único para a aba + ); + + // Adicionar ao tracking para auto-refresh + activePreviewPages.set(pageId, { + category: category, + slug: slug, + windowRef: previewWindow + }); + + console.log(`Page ${pageId} added to active preview tracking`); } else { showToast(result.message || 'Erro ao gerar preview', 'error'); }