Files
2026-01-30 11:18:56 -03:00

223 lines
8.3 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.RateLimiting; // Rate Limit
using System.Threading.RateLimiting; // Rate Limit
using System.Text;
using MotoresArgentinosV2.Core.Interfaces;
using MotoresArgentinosV2.Infrastructure.Data;
using MotoresArgentinosV2.Infrastructure.Services;
using MotoresArgentinosV2.Core.Models;
using DotNetEnv;
using Microsoft.AspNetCore.HttpOverrides;
// 🔒 ENV VARS
Env.Load();
var builder = WebApplication.CreateBuilder(args);
// Forzar a la configuración a leer las variables
builder.Configuration.AddEnvironmentVariables();
// 🔒 KESTREL HARDENING
builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false);
// LOGGING
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// 🔒 CORS POLICY
var frontendUrls = (builder.Configuration["AppSettings:FrontendUrl"] ?? "http://localhost:5173").Split(',');
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
policy => policy.WithOrigins(frontendUrls)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
// FORWARDED HEADERS (CRÍTICO PARA DOCKER/NGINX)
// Por defecto, .NET solo confía en localhost. En Docker, Nginx tiene otra IP.
// Debemos limpiar las redes conocidas para que confíe en el proxy interno de Docker.
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
// 🔒 RATE LIMITING
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
// En producción detrás de Nginx, RemoteIpAddress será la IP real del usuario.
// Si por alguna razón falla (ej: conexión directa local), usamos "unknown".
var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
// Si es Loopback (localhost), sin límites (útil para dev)
if (System.Net.IPAddress.IsLoopback(context.Connection.RemoteIpAddress!))
{
return RateLimitPartition.GetNoLimiter("loopback");
}
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: remoteIp, // Clave correcta: IP del usuario
factory: _ => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
QueueLimit = 2,
Window = TimeSpan.FromMinutes(1)
});
});
options.AddPolicy("AuthPolicy", context =>
{
// Si es localhost, SIN LÍMITES (Evita auto-bloqueo en desarrollo)
var remoteIp = context.Connection.RemoteIpAddress;
if (remoteIp != null && System.Net.IPAddress.IsLoopback(remoteIp))
{
return RateLimitPartition.GetNoLimiter("loopback_auth");
}
return RateLimitPartition.GetFixedWindowLimiter("auth_limit", _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
});
});
});
// 🛡️ 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)
builder.Services.AddDbContext<EldiaDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("eldia")));
builder.Services.AddDbContext<MotoresV2DbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MotoresV2"),
sqlOptions => sqlOptions.EnableRetryOnFailure()));
// SERVICIOS
builder.Services.AddScoped<IAvisosLegacyService, AvisosLegacyService>();
builder.Services.AddScoped<IOperacionesLegacyService, OperacionesLegacyService>();
builder.Services.AddScoped<IUsuariosLegacyService, UsuariosLegacyService>();
builder.Services.AddScoped<IPasswordService, PasswordService>();
builder.Services.AddScoped<IIdentityService, IdentityService>();
builder.Services.AddScoped<ILegacyPaymentService, LegacyPaymentService>();
builder.Services.AddScoped<IPaymentService, MercadoPagoService>();
builder.Services.AddScoped<IAdSyncService, AdSyncService>();
builder.Services.AddScoped<INotificationService, NotificationService>();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.Configure<MailSettings>(builder.Configuration.GetSection("SmtpSettings"));
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddScoped<IImageStorageService, ImageStorageService>();
builder.Services.AddHostedService<AdExpirationService>();
builder.Services.AddHostedService<TokenCleanupService>();
// 🔒 JWT AUTH
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key Missing");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
// 🟢 LEER TOKEN DESDE COOKIE
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Buscar el token en la cookie llamada "accessToken"
var accessToken = context.Request.Cookies["accessToken"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Middleware de Manejo Global de Excepciones (Debe ser el primero para atrapar todo)
app.UseMiddleware<MotoresArgentinosV2.API.Middleware.ExceptionHandlingMiddleware>();
// USAR EL MIDDLEWARE DE HEADERS
app.UseForwardedHeaders();
// 🔒 HEADERS DE SEGURIDAD MIDDLEWARE
app.Use(async (context, next) =>
{
context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
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
string csp = "default-src 'self'; " +
"img-src 'self' data: https: blob:; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"connect-src 'self' https: ws: wss:; " +
"object-src 'none'; " +
"base-uri 'self'; " +
"form-action 'self' https://developers-ventasonline.payway.com.ar; " +
"frame-ancestors 'none';";
context.Response.Headers.Append("Content-Security-Policy", csp);
context.Response.Headers.Remove("Server");
context.Response.Headers.Remove("X-Powered-By");
await next();
});
// PIPELINE
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
// 🔒 HSTS en Producción
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// 🔒 APLICAR CORS & RATE LIMIT
app.UseCors("AllowSpecificOrigin");
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();