Feat: Ajustes de seguridad
This commit is contained in:
@@ -11,7 +11,7 @@ namespace MotoresArgentinosV2.API.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
// CORRECCIÓN: Se quitó [EnableRateLimiting("AuthPolicy")] de aquí para no bloquear /me ni /logout
|
[EnableRateLimiting("AuthPolicy")]
|
||||||
public class AuthController : ControllerBase
|
public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IIdentityService _identityService;
|
private readonly IIdentityService _identityService;
|
||||||
@@ -33,7 +33,7 @@ public class AuthController : ControllerBase
|
|||||||
var cookieOptions = new CookieOptions
|
var cookieOptions = new CookieOptions
|
||||||
{
|
{
|
||||||
HttpOnly = true, // Seguridad: JS no puede leer esto
|
HttpOnly = true, // Seguridad: JS no puede leer esto
|
||||||
Expires = DateTime.UtcNow.AddDays(7),
|
Expires = DateTime.UtcNow.AddMinutes(15),
|
||||||
Secure = true, // Solo HTTPS (localhost con https cuenta)
|
Secure = true, // Solo HTTPS (localhost con https cuenta)
|
||||||
SameSite = SameSiteMode.Strict,
|
SameSite = SameSiteMode.Strict,
|
||||||
IsEssential = true
|
IsEssential = true
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MotoresArgentinosV2.Infrastructure.Data;
|
|
||||||
using MotoresArgentinosV2.Core.Entities;
|
|
||||||
using MotoresArgentinosV2.Core.Interfaces;
|
|
||||||
|
|
||||||
namespace MotoresArgentinosV2.API.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class SeedController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly MotoresV2DbContext _context;
|
|
||||||
private readonly IPasswordService _passwordService;
|
|
||||||
|
|
||||||
public SeedController(MotoresV2DbContext context, IPasswordService passwordService)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_passwordService = passwordService;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("database")]
|
|
||||||
public async Task<IActionResult> SeedDatabase()
|
|
||||||
{
|
|
||||||
// 1. Asegurar Marcas y Modelos
|
|
||||||
if (!await _context.Brands.AnyAsync())
|
|
||||||
{
|
|
||||||
var toyota = new Brand { VehicleTypeID = 1, Name = "Toyota" };
|
|
||||||
var ford = new Brand { VehicleTypeID = 1, Name = "Ford" };
|
|
||||||
var vw = new Brand { VehicleTypeID = 1, Name = "Volkswagen" };
|
|
||||||
var honda = new Brand { VehicleTypeID = 2, Name = "Honda" };
|
|
||||||
var yamaha = new Brand { VehicleTypeID = 2, Name = "Yamaha" };
|
|
||||||
|
|
||||||
_context.Brands.AddRange(toyota, ford, vw, honda, yamaha);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_context.Models.AddRange(
|
|
||||||
new Model { BrandID = toyota.BrandID, Name = "Corolla" },
|
|
||||||
new Model { BrandID = toyota.BrandID, Name = "Hilux" },
|
|
||||||
new Model { BrandID = ford.BrandID, Name = "Ranger" },
|
|
||||||
new Model { BrandID = vw.BrandID, Name = "Amarok" },
|
|
||||||
new Model { BrandID = honda.BrandID, Name = "Wave 110" },
|
|
||||||
new Model { BrandID = yamaha.BrandID, Name = "FZ FI" }
|
|
||||||
);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Crear Usuarios de Prueba
|
|
||||||
var testUser = await _context.Users.FirstOrDefaultAsync(u => u.UserName == "testuser");
|
|
||||||
if (testUser == null)
|
|
||||||
{
|
|
||||||
testUser = new User
|
|
||||||
{
|
|
||||||
UserName = "testuser",
|
|
||||||
Email = "test@motores.com.ar",
|
|
||||||
PasswordHash = _passwordService.HashPassword("test123"),
|
|
||||||
FirstName = "Usuario",
|
|
||||||
LastName = "Prueba",
|
|
||||||
MigrationStatus = 1,
|
|
||||||
UserType = 1
|
|
||||||
};
|
|
||||||
_context.Users.Add(testUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
var adminUser = await _context.Users.FirstOrDefaultAsync(u => u.UserName == "admin");
|
|
||||||
if (adminUser == null)
|
|
||||||
{
|
|
||||||
adminUser = new User
|
|
||||||
{
|
|
||||||
UserName = "admin",
|
|
||||||
Email = "admin@motoresargentinos.com.ar",
|
|
||||||
PasswordHash = _passwordService.HashPassword("admin123"),
|
|
||||||
FirstName = "Admin",
|
|
||||||
LastName = "Motores",
|
|
||||||
MigrationStatus = 1,
|
|
||||||
UserType = 3 // ADMIN
|
|
||||||
};
|
|
||||||
_context.Users.Add(adminUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
// 3. Crear Avisos de Prueba
|
|
||||||
if (!await _context.Ads.AnyAsync())
|
|
||||||
{
|
|
||||||
var brands = await _context.Brands.ToListAsync();
|
|
||||||
var models = await _context.Models.ToListAsync();
|
|
||||||
|
|
||||||
var ad1 = new Ad
|
|
||||||
{
|
|
||||||
UserID = testUser.UserID,
|
|
||||||
VehicleTypeID = 1,
|
|
||||||
BrandID = brands.First(b => b.Name == "Toyota").BrandID,
|
|
||||||
ModelID = models.First(m => m.Name == "Corolla").ModelID,
|
|
||||||
VersionName = "Toyota Corolla 1.8 XLI",
|
|
||||||
Year = 2022,
|
|
||||||
KM = 15000,
|
|
||||||
Price = 25000,
|
|
||||||
Currency = "USD",
|
|
||||||
Description = "Excelente estado, único dueño. Service al día.",
|
|
||||||
StatusID = 4, // Activo
|
|
||||||
IsFeatured = true,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
PublishedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
var ad2 = new Ad
|
|
||||||
{
|
|
||||||
UserID = testUser.UserID,
|
|
||||||
VehicleTypeID = 2,
|
|
||||||
BrandID = brands.First(b => b.Name == "Honda").BrandID,
|
|
||||||
ModelID = models.First(m => m.Name == "Wave 110").ModelID,
|
|
||||||
VersionName = "Honda Wave 110 S",
|
|
||||||
Year = 2023,
|
|
||||||
KM = 2500,
|
|
||||||
Price = 1800,
|
|
||||||
Currency = "USD",
|
|
||||||
Description = "Impecable, como nueva. Muy económica.",
|
|
||||||
StatusID = 4, // Activo
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
PublishedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
var ad3 = new Ad
|
|
||||||
{
|
|
||||||
UserID = testUser.UserID,
|
|
||||||
VehicleTypeID = 1,
|
|
||||||
BrandID = brands.First(b => b.Name == "Ford").BrandID,
|
|
||||||
ModelID = models.First(m => m.Name == "Ranger").ModelID,
|
|
||||||
VersionName = "Ford Ranger Limited 4x4",
|
|
||||||
Year = 2021,
|
|
||||||
KM = 35000,
|
|
||||||
Price = 42000,
|
|
||||||
Currency = "USD",
|
|
||||||
Description = "Camioneta impecable, lista para transferir.",
|
|
||||||
StatusID = 3, // Moderacion Pendiente
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
_context.Ads.AddRange(ad1, ad2, ad3);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
// Agregar fotos
|
|
||||||
_context.AdPhotos.AddRange(
|
|
||||||
new AdPhoto { AdID = ad1.AdID, FilePath = "https://images.unsplash.com/photo-1621007947382-bb3c3994e3fb?auto=format&fit=crop&q=80&w=1200", IsCover = true },
|
|
||||||
new AdPhoto { AdID = ad1.AdID, FilePath = "https://images.unsplash.com/photo-1590362891991-f776e933a68e?auto=format&fit=crop&q=80&w=1200" },
|
|
||||||
new AdPhoto { AdID = ad1.AdID, FilePath = "https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?auto=format&fit=crop&q=80&w=1200" },
|
|
||||||
new AdPhoto { AdID = ad2.AdID, FilePath = "https://images.unsplash.com/photo-1558981403-c5f91cbba527?auto=format&fit=crop&q=80&w=800", IsCover = true },
|
|
||||||
new AdPhoto { AdID = ad3.AdID, FilePath = "https://images.unsplash.com/photo-1533473359331-0135ef1b58bf?auto=format&fit=crop&q=80&w=1200", IsCover = true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Agregar Características Técnicas
|
|
||||||
_context.AdFeatures.AddRange(
|
|
||||||
new AdFeature { AdID = ad1.AdID, FeatureKey = "Combustible", FeatureValue = "Nafta" },
|
|
||||||
new AdFeature { AdID = ad1.AdID, FeatureKey = "Transmision", FeatureValue = "Automática" },
|
|
||||||
new AdFeature { AdID = ad1.AdID, FeatureKey = "Color", FeatureValue = "Blanco" },
|
|
||||||
new AdFeature { AdID = ad2.AdID, FeatureKey = "Combustible", FeatureValue = "Nafta" },
|
|
||||||
new AdFeature { AdID = ad2.AdID, FeatureKey = "Color", FeatureValue = "Rojo" }
|
|
||||||
);
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok("Database seeded successfully with Features and Multiple Photos");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("reset")]
|
|
||||||
public async Task<IActionResult> ResetDatabase()
|
|
||||||
{
|
|
||||||
_context.ChangeTracker.Clear();
|
|
||||||
await _context.Database.ExecuteSqlRawAsync("DELETE FROM Transactions");
|
|
||||||
await _context.Database.ExecuteSqlRawAsync("DELETE FROM AdPhotos");
|
|
||||||
await _context.Database.ExecuteSqlRawAsync("DELETE FROM AdFeatures");
|
|
||||||
await _context.Database.ExecuteSqlRawAsync("DELETE FROM Ads");
|
|
||||||
await _context.Database.ExecuteSqlRawAsync("DELETE FROM Models");
|
|
||||||
await _context.Database.ExecuteSqlRawAsync("DELETE FROM Brands");
|
|
||||||
|
|
||||||
return await SeedDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MotoresArgentinosV2.API.Middleware;
|
||||||
|
|
||||||
|
public class ExceptionHandlingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||||
|
private readonly IHostEnvironment _env;
|
||||||
|
|
||||||
|
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment env)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
_env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await HandleExceptionAsync(context, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||||
|
{
|
||||||
|
// Loguear el error real con stack trace completo
|
||||||
|
_logger.LogError(exception, "Error no controlado procesando la solicitud: {Method} {Path}", context.Request.Method, context.Request.Path);
|
||||||
|
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
status = context.Response.StatusCode,
|
||||||
|
message = "Ocurrió un error interno en el servidor. Por favor, intente nuevamente más tarde.",
|
||||||
|
// En desarrollo mostramos el detalle, en producción ocultamos todo
|
||||||
|
detail = _env.IsDevelopment() ? exception.Message : null
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(response);
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,25 +78,28 @@ builder.Services.AddRateLimiter(options =>
|
|||||||
|
|
||||||
options.AddPolicy("AuthPolicy", context =>
|
options.AddPolicy("AuthPolicy", context =>
|
||||||
{
|
{
|
||||||
// Si es localhost, SIN LÍMITES
|
// Si es localhost, SIN LÍMITES (Evita auto-bloqueo en desarrollo)
|
||||||
var remoteIp = context.Connection.RemoteIpAddress;
|
var remoteIp = context.Connection.RemoteIpAddress;
|
||||||
if (System.Net.IPAddress.IsLoopback(remoteIp!))
|
if (remoteIp != null && System.Net.IPAddress.IsLoopback(remoteIp))
|
||||||
{
|
{
|
||||||
return RateLimitPartition.GetNoLimiter("loopback_auth");
|
return RateLimitPartition.GetNoLimiter("loopback_auth");
|
||||||
}
|
}
|
||||||
|
|
||||||
return RateLimitPartition.GetFixedWindowLimiter(
|
return RateLimitPartition.GetFixedWindowLimiter("auth_limit", _ => new FixedWindowRateLimiterOptions
|
||||||
partitionKey: remoteIp?.ToString() ?? "unknown",
|
{
|
||||||
factory: _ => new FixedWindowRateLimiterOptions
|
PermitLimit = 5,
|
||||||
{
|
Window = TimeSpan.FromMinutes(1),
|
||||||
AutoReplenishment = true,
|
QueueLimit = 0
|
||||||
PermitLimit = 5, // 5 intentos por minuto para IPs externas
|
});
|
||||||
QueueLimit = 0,
|
|
||||||
Window = TimeSpan.FromMinutes(1)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🛡️ SEGURIDAD: Evitar que el host se caiga si un servicio de fondo falla
|
||||||
|
builder.Services.Configure<HostOptions>(options =>
|
||||||
|
{
|
||||||
|
options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
|
||||||
|
});
|
||||||
|
|
||||||
// DB CONTEXTS (Legacy unificado en eldia)
|
// DB CONTEXTS (Legacy unificado en eldia)
|
||||||
builder.Services.AddDbContext<EldiaDbContext>(options =>
|
builder.Services.AddDbContext<EldiaDbContext>(options =>
|
||||||
options.UseSqlServer(builder.Configuration.GetConnectionString("eldia")));
|
options.UseSqlServer(builder.Configuration.GetConnectionString("eldia")));
|
||||||
@@ -119,6 +122,7 @@ builder.Services.Configure<MailSettings>(builder.Configuration.GetSection("SmtpS
|
|||||||
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
||||||
builder.Services.AddScoped<IImageStorageService, ImageStorageService>();
|
builder.Services.AddScoped<IImageStorageService, ImageStorageService>();
|
||||||
builder.Services.AddHostedService<AdExpirationService>();
|
builder.Services.AddHostedService<AdExpirationService>();
|
||||||
|
builder.Services.AddHostedService<TokenCleanupService>();
|
||||||
|
|
||||||
// 🔒 JWT AUTH
|
// 🔒 JWT AUTH
|
||||||
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key Missing");
|
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key Missing");
|
||||||
@@ -158,8 +162,10 @@ builder.Services.AddSwaggerGen();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// USAR EL MIDDLEWARE AL PRINCIPIO
|
// Middleware de Manejo Global de Excepciones (Debe ser el primero para atrapar todo)
|
||||||
// Debe ser lo primero para que el RateLimiter y los Logs vean la IP real
|
app.UseMiddleware<MotoresArgentinosV2.API.Middleware.ExceptionHandlingMiddleware>();
|
||||||
|
|
||||||
|
// USAR EL MIDDLEWARE DE HEADERS
|
||||||
app.UseForwardedHeaders();
|
app.UseForwardedHeaders();
|
||||||
|
|
||||||
// 🔒 HEADERS DE SEGURIDAD MIDDLEWARE
|
// 🔒 HEADERS DE SEGURIDAD MIDDLEWARE
|
||||||
@@ -170,6 +176,9 @@ app.Use(async (context, next) =>
|
|||||||
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
|
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
|
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
|
// Permissions-Policy: Bloquear funcionalidades sensibles del navegador no usadas
|
||||||
|
context.Response.Headers.Append("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
|
||||||
|
|
||||||
// CSP adaptada para permitir pagos en Payway y WebSockets de Vite
|
// CSP adaptada para permitir pagos en Payway y WebSockets de Vite
|
||||||
string csp = "default-src 'self'; " +
|
string csp = "default-src 'self'; " +
|
||||||
"img-src 'self' data: https: blob:; " +
|
"img-src 'self' data: https: blob:; " +
|
||||||
|
|||||||
@@ -43,14 +43,19 @@ public class AdExpirationService : BackgroundService
|
|||||||
await ProcessWeeklyStatsAsync();
|
await ProcessWeeklyStatsAsync();
|
||||||
await ProcessPaymentRemindersAsync();
|
await ProcessPaymentRemindersAsync();
|
||||||
await ProcessUnreadMessagesRemindersAsync();
|
await ProcessUnreadMessagesRemindersAsync();
|
||||||
|
|
||||||
|
// Ejecutar cada 1 hora
|
||||||
|
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error CRÍTICO en ciclo de mantenimiento.");
|
_logger.LogError(ex, "Error CRÍTICO en ciclo de mantenimiento.");
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecutar cada 1 hora
|
|
||||||
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MotoresArgentinosV2.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace MotoresArgentinosV2.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class TokenCleanupService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<TokenCleanupService> _logger;
|
||||||
|
private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(24); // Ejecutar cada 24hs
|
||||||
|
|
||||||
|
public TokenCleanupService(IServiceProvider serviceProvider, ILogger<TokenCleanupService> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Servicio de Limpieza de Tokens iniciado.");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CleanExpiredTokensAsync(stoppingToken);
|
||||||
|
// Esperar hasta la próxima ejecución
|
||||||
|
await Task.Delay(_cleanupInterval, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// El servicio se está deteniendo, es normal
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error durante la limpieza de tokens.");
|
||||||
|
// Si hay un error, esperamos un poco antes de reintentar (evitar bucle infinito de errores)
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CleanExpiredTokensAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
|
||||||
|
|
||||||
|
// Definir criterios de limpieza
|
||||||
|
var cutoffDate = DateTime.UtcNow; // Tokens ya expirados
|
||||||
|
var revokedCutoff = DateTime.UtcNow.AddDays(-30); // Tokens revocados hace más de 30 días (auditoría)
|
||||||
|
|
||||||
|
_logger.LogInformation("Ejecutando limpieza de RefreshTokens expirados o antiguos...");
|
||||||
|
|
||||||
|
// Opción 1: Borrado con SQL Raw para eficiencia en lotes grandes
|
||||||
|
// Asumimos que la tabla se llama 'RefreshTokens' en la DB
|
||||||
|
var rowsAffected = await context.Database.ExecuteSqlRawAsync(
|
||||||
|
"DELETE FROM [RefreshTokens] WHERE [Expires] < @cutoffDate OR ([Revoked] IS NOT NULL AND [Revoked] < @revokedCutoff)",
|
||||||
|
new[] {
|
||||||
|
new Microsoft.Data.SqlClient.SqlParameter("@cutoffDate", cutoffDate),
|
||||||
|
new Microsoft.Data.SqlClient.SqlParameter("@revokedCutoff", revokedCutoff)
|
||||||
|
},
|
||||||
|
stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Limpieza completada. {Count} tokens eliminados.", rowsAffected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ public class TokenService : ITokenService
|
|||||||
new Claim(ClaimTypes.Email, user.Email),
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
new Claim(ClaimTypes.Role, user.UserType == 3 ? "Admin" : "User")
|
new Claim(ClaimTypes.Role, user.UserType == 3 ? "Admin" : "User")
|
||||||
}),
|
}),
|
||||||
Expires = DateTime.UtcNow.AddHours(8),
|
Expires = DateTime.UtcNow.AddMinutes(15),
|
||||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
|
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
|
||||||
Issuer = _config["Jwt:Issuer"],
|
Issuer = _config["Jwt:Issuer"],
|
||||||
Audience = _config["Jwt:Audience"]
|
Audience = _config["Jwt:Audience"]
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
|
# Seguridad: Limitar tamaño de subida para prevenir DoS
|
||||||
|
client_max_body_size 20M;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
--animate-fade-in-up: fade-in-up 0.5s ease-out;
|
--animate-fade-in-up: fade-in-up 0.5s ease-out;
|
||||||
--animate-glow: glow 2s infinite alternate;
|
--animate-glow: glow 2s infinite alternate;
|
||||||
|
--animate-fade-in: fade-in 0.3s ease-out forwards;
|
||||||
|
--animate-scale-up: scale-up 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||||
|
|
||||||
@keyframes fade-in-up {
|
@keyframes fade-in-up {
|
||||||
from {
|
from {
|
||||||
@@ -52,16 +54,22 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
|
||||||
animation: fade-in 0.3s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-scale-up {
|
|
||||||
animation: scale-up 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Clases de utilidad personalizadas (fuera de @theme) */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: var(--animate-fade-in);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scale-up {
|
||||||
|
animation: var(--animate-scale-up);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: var(--animate-fade-in-up);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
background-color: var(--color-dark-bg);
|
background-color: var(--color-dark-bg);
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -8,4 +8,7 @@ export default defineConfig({
|
|||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
build: {
|
||||||
|
sourcemap: false, // Seguridad: Ocultar código fuente en producción
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user