From 331b3de374d592bb4799baa428752c920ae3a8d7 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sun, 31 Aug 2025 14:44:41 -0300 Subject: [PATCH] fix: performance optimizations --- src/BCards.Web/Controllers/AdminController.cs | 9 +++ .../Middleware/SessionTimeoutMiddleware.cs | 39 ++++++++++ .../Middleware/SmartCacheMiddleware.cs | 75 +++++++++++++++++++ src/BCards.Web/Program.cs | 59 ++++++++++++++- src/BCards.Web/Views/Admin/Dashboard.cshtml | 55 ++++++++++++++ 5 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 src/BCards.Web/Middleware/SessionTimeoutMiddleware.cs create mode 100644 src/BCards.Web/Middleware/SmartCacheMiddleware.cs diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index ea38e79..4252733 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -1078,4 +1078,13 @@ public class AdminController : Controller if (string.IsNullOrWhiteSpace(model.TwitterUrl) || model.TwitterUrl.Trim().Length <= 1) model.TwitterUrl = string.Empty; } + + // 🔥 OTIMIZAÇÃO: Endpoint para manter a sessão do usuário ativa + [HttpPost] + [Route("KeepAlive")] + public IActionResult KeepAlive() + { + _logger.LogInformation("KeepAlive endpoint triggered for user {User}", User.Identity?.Name ?? "Anonymous"); + return Json(new { status = "session_extended" }); + } } \ No newline at end of file diff --git a/src/BCards.Web/Middleware/SessionTimeoutMiddleware.cs b/src/BCards.Web/Middleware/SessionTimeoutMiddleware.cs new file mode 100644 index 0000000..888d6dc --- /dev/null +++ b/src/BCards.Web/Middleware/SessionTimeoutMiddleware.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace BCards.Web.Middleware +{ + public class SessionTimeoutMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public SessionTimeoutMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + await _next(context); + + // Após a requisição, verifica se foi um 401 Unauthorized em uma chamada AJAX + if (context.Response.StatusCode == 401 && IsAjaxRequest(context.Request)) + { + _logger.LogWarning("Session timeout detected for user {User} on path {Path}. AJAX request received 401, which may cause 503 errors on the client-side proxy.", + context.User.Identity?.Name ?? "Unknown", + context.Request.Path); + + // Adiciona um header para o cliente (JavaScript) saber que precisa redirecionar + context.Response.Headers["X-Redirect-To"] = "/Auth/Login?reason=session_expired"; + } + } + + private bool IsAjaxRequest(HttpRequest request) + { + return "XMLHttpRequest".Equals(request.Headers["X-Requested-With"], System.StringComparison.Ordinal); + } + } +} \ No newline at end of file diff --git a/src/BCards.Web/Middleware/SmartCacheMiddleware.cs b/src/BCards.Web/Middleware/SmartCacheMiddleware.cs new file mode 100644 index 0000000..1eb3362 --- /dev/null +++ b/src/BCards.Web/Middleware/SmartCacheMiddleware.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using BCards.Web.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace BCards.Web.Middleware +{ + public class SmartCacheMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public SmartCacheMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + var path = context.Request.Path.Value ?? string.Empty; + + // Aplica a lógica de cache apenas para as rotas de exibição de página + if (path.StartsWith("/page/", StringComparison.OrdinalIgnoreCase) && context.Request.Method == HttpMethods.Get) + { + var pathSegments = path.Trim('/').Split('/'); + if (pathSegments.Length == 3 && pathSegments[0].Equals("page", StringComparison.OrdinalIgnoreCase)) + { + var category = pathSegments[1]; + var slug = pathSegments[2]; + var isPreview = context.Request.Query.ContainsKey("preview"); + + // Para preview, NUNCA usar cache + if (isPreview) + { + _logger.LogInformation("SmartCache: Applying NO-CACHE for preview page {Category}/{Slug}", category, slug); + SetNoCacheHeaders(context.Response); + } + else + { + // Para páginas públicas, a existência de uma LivePage significa que ela está ativa e pode ser cacheada. + var livePageService = context.RequestServices.GetRequiredService(); + var livePage = await livePageService.GetByCategoryAndSlugAsync(category, slug); + + if (livePage != null) + { + // --- CENÁRIO 1: PÁGINA LIVE (ATIVA) --- + _logger.LogInformation("SmartCache: Applying 1-hour PUBLIC cache for live page {Category}/{Slug}", category, slug); + context.Response.Headers["Cache-Control"] = "public, max-age=3600"; + context.Response.Headers.Append("Vary", "Accept-Encoding"); + } + else + { + // --- CENÁRIO 2: PÁGINA NÃO-ATIVA OU NÃO ENCONTRADA --- + // A requisição vai para o UserPageController, que não deve ter cache. + _logger.LogInformation("SmartCache: Applying NO-CACHE for non-live page {Category}/{Slug}", category, slug); + SetNoCacheHeaders(context.Response); + } + } + } + } + + await _next(context); + } + + private void SetNoCacheHeaders(HttpResponse response) + { + response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "0"; + } + } +} diff --git a/src/BCards.Web/Program.cs b/src/BCards.Web/Program.cs index 2df4bfd..5f4f21c 100644 --- a/src/BCards.Web/Program.cs +++ b/src/BCards.Web/Program.cs @@ -94,6 +94,26 @@ builder.Services.Configure(options => options.ForwardLimit = null; }); +// 🔥 OTIMIZAÇÃO: Sistema de Compressão de Response (Brotli + Gzip) +builder.Services.AddResponseCompression(options => +{ + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); + options.MimeTypes = Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults.MimeTypes.Concat( + new[] { "application/javascript", "application/json", "application/xml", "text/css", "text/plain", "image/svg+xml" }); +}); + +builder.Services.Configure(options => +{ + options.Level = System.IO.Compression.CompressionLevel.Optimal; +}); + +builder.Services.Configure(options => +{ + options.Level = System.IO.Compression.CompressionLevel.Optimal; +}); + // Add services to the container. builder.Services.AddControllersWithViews() .AddRazorRuntimeCompilation() @@ -104,10 +124,21 @@ builder.Services.AddControllersWithViews() builder.Services.Configure( builder.Configuration.GetSection("MongoDb")); +// 🔥 OTIMIZAÇÃO: Pool de conexões do MongoDB builder.Services.AddSingleton(serviceProvider => { var settings = serviceProvider.GetRequiredService>().Value; - return new MongoClient(settings.ConnectionString); + var connectionString = settings.ConnectionString; + + // Adiciona configurações de pool de conexões se não existirem + if (!connectionString.Contains("maxPoolSize")) + { + var separator = connectionString.Contains("?") ? "&" : "?"; + connectionString += $"{separator}maxPoolSize=200&minPoolSize=20&maxIdleTimeMS=300000&socketTimeoutMS=60000&connectTimeoutMS=30000"; + } + + Log.Information("Connecting to MongoDB with optimized pool settings."); + return new MongoClient(connectionString); }); builder.Services.AddScoped(serviceProvider => @@ -142,7 +173,8 @@ builder.Services.AddAuthentication(options => { options.LoginPath = "/Auth/Login"; options.LogoutPath = "/Auth/Logout"; - options.ExpireTimeSpan = TimeSpan.FromDays(30); + // 🔥 OTIMIZAÇÃO: Timeout de sessão estendido para 8h + options.ExpireTimeSpan = TimeSpan.FromHours(8); options.SlidingExpiration = true; }) .AddGoogle(options => @@ -343,6 +375,25 @@ if (!app.Environment.IsDevelopment()) }); } +// 🔥 OTIMIZAÇÃO: Adiciona headers de segurança +app.Use(async (context, next) => +{ + context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); + context.Response.Headers.Append("X-Frame-Options", "DENY"); + context.Response.Headers.Append("Referrer-Policy", "no-referrer"); + context.Response.Headers.Append("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); + + // Remove headers que expõem a tecnologia + context.Response.OnStarting(() => + { + context.Response.Headers.Remove("Server"); + context.Response.Headers.Remove("X-Powered-By"); + return Task.CompletedTask; + }); + + await next(); +}); + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { @@ -351,6 +402,10 @@ if (!app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); + +// 🔥 OTIMIZAÇÃO: Ativar compressão de response +app.UseResponseCompression(); + app.UseStaticFiles(); app.UseRouting(); diff --git a/src/BCards.Web/Views/Admin/Dashboard.cshtml b/src/BCards.Web/Views/Admin/Dashboard.cshtml index c944abc..278a3c3 100644 --- a/src/BCards.Web/Views/Admin/Dashboard.cshtml +++ b/src/BCards.Web/Views/Admin/Dashboard.cshtml @@ -431,6 +431,61 @@ } return container; } + + // 🔥 OTIMIZAÇÃO: Sistema de KeepAlive para evitar timeout de sessão + (function() { + let inactivityTimer; + const inactivityTimeout = 5 * 60 * 1000; // 5 minutos + const sessionTimeout = 8 * 60 * 60 * 1000; // 8 horas (deve ser igual ao do cookie) + let sessionStartTime = Date.now(); + + function resetInactivityTimer() { + clearTimeout(inactivityTimer); + // Só agenda o próximo ping se a sessão não estiver prestes a expirar + if (Date.now() - sessionStartTime < sessionTimeout) { + inactivityTimer = setTimeout(keepSessionAlive, inactivityTimeout); + } + } + + async function keepSessionAlive() { + // Verifica se o tempo total da sessão já não foi excedido + if (Date.now() - sessionStartTime >= sessionTimeout) { + console.log("Session has reached its absolute maximum lifetime. Redirecting."); + window.location.href = '/Auth/Login?reason=session_expired'; + return; + } + + try { + const response = await fetch('/Admin/KeepAlive', { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (response.ok) { + const result = await response.json(); + if (result.status === 'session_extended') { + console.log('Session extended at ' + new Date().toLocaleTimeString()); + resetInactivityTimer(); // Reset timer after successful ping + } + } else if (response.status === 401 || response.status === 302 || response.redirected) { + // Se não autorizado ou redirecionado para login, força o reload + window.location.href = '/Auth/Login?reason=session_expired'; + } + } catch (error) { + console.error('KeepAlive request failed:', error); + // Não reseta o timer se a rede falhar, para tentar novamente depois + } + } + + // Inicia o sistema + document.addEventListener('mousemove', resetInactivityTimer, { passive: true }); + document.addEventListener('keypress', resetInactivityTimer, { passive: true }); + document.addEventListener('click', resetInactivityTimer, { passive: true }); + resetInactivityTimer(); + })(); }