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)
|
if (string.IsNullOrWhiteSpace(model.TwitterUrl) || model.TwitterUrl.Trim().Length <= 1)
|
||||||
model.TwitterUrl = string.Empty;
|
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;
|
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.
|
// Add services to the container.
|
||||||
builder.Services.AddControllersWithViews()
|
builder.Services.AddControllersWithViews()
|
||||||
.AddRazorRuntimeCompilation()
|
.AddRazorRuntimeCompilation()
|
||||||
@ -104,10 +124,21 @@ builder.Services.AddControllersWithViews()
|
|||||||
builder.Services.Configure<MongoDbSettings>(
|
builder.Services.Configure<MongoDbSettings>(
|
||||||
builder.Configuration.GetSection("MongoDb"));
|
builder.Configuration.GetSection("MongoDb"));
|
||||||
|
|
||||||
|
// 🔥 OTIMIZAÇÃO: Pool de conexões do MongoDB
|
||||||
builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
|
builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
|
||||||
{
|
{
|
||||||
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value;
|
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 =>
|
builder.Services.AddScoped(serviceProvider =>
|
||||||
@ -142,7 +173,8 @@ builder.Services.AddAuthentication(options =>
|
|||||||
{
|
{
|
||||||
options.LoginPath = "/Auth/Login";
|
options.LoginPath = "/Auth/Login";
|
||||||
options.LogoutPath = "/Auth/Logout";
|
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;
|
options.SlidingExpiration = true;
|
||||||
})
|
})
|
||||||
.AddGoogle(options =>
|
.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.
|
// Configure the HTTP request pipeline.
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@ -351,6 +402,10 @@ if (!app.Environment.IsDevelopment())
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
// 🔥 OTIMIZAÇÃO: Ativar compressão de response
|
||||||
|
app.UseResponseCompression();
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|||||||
@ -431,6 +431,61 @@
|
|||||||
}
|
}
|
||||||
return container;
|
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>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user