fix: performance optimizations
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 2s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 14m13s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m16s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 2s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 14m13s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m16s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
This commit is contained in:
parent
7cc8f46a1a
commit
331b3de374
@ -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" });
|
||||
}
|
||||
}
|
||||
39
src/BCards.Web/Middleware/SessionTimeoutMiddleware.cs
Normal file
39
src/BCards.Web/Middleware/SessionTimeoutMiddleware.cs
Normal file
@ -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<SessionTimeoutMiddleware> _logger;
|
||||
|
||||
public SessionTimeoutMiddleware(RequestDelegate next, ILogger<SessionTimeoutMiddleware> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/BCards.Web/Middleware/SmartCacheMiddleware.cs
Normal file
75
src/BCards.Web/Middleware/SmartCacheMiddleware.cs
Normal file
@ -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<SmartCacheMiddleware> _logger;
|
||||
|
||||
public SmartCacheMiddleware(RequestDelegate next, ILogger<SmartCacheMiddleware> 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<ILivePageService>();
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,6 +94,26 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
options.ForwardLimit = null;
|
||||
});
|
||||
|
||||
// 🔥 OTIMIZAÇÃO: Sistema de Compressão de Response (Brotli + Gzip)
|
||||
builder.Services.AddResponseCompression(options =>
|
||||
{
|
||||
options.EnableForHttps = true;
|
||||
options.Providers.Add<Microsoft.AspNetCore.ResponseCompression.BrotliCompressionProvider>();
|
||||
options.Providers.Add<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProvider>();
|
||||
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<Microsoft.AspNetCore.ResponseCompression.BrotliCompressionProviderOptions>(options =>
|
||||
{
|
||||
options.Level = System.IO.Compression.CompressionLevel.Optimal;
|
||||
});
|
||||
|
||||
builder.Services.Configure<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions>(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<MongoDbSettings>(
|
||||
builder.Configuration.GetSection("MongoDb"));
|
||||
|
||||
// 🔥 OTIMIZAÇÃO: Pool de conexões do MongoDB
|
||||
builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
|
||||
{
|
||||
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().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();
|
||||
|
||||
@ -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();
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user