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(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(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 var remoteIp = context.Connection.RemoteIpAddress; if (System.Net.IPAddress.IsLoopback(remoteIp!)) { return RateLimitPartition.GetNoLimiter("loopback_auth"); } return RateLimitPartition.GetFixedWindowLimiter( partitionKey: remoteIp?.ToString() ?? "unknown", factory: _ => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 5, // 5 intentos por minuto para IPs externas QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }); }); }); // DB CONTEXTS (Legacy unificado en eldia) builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("eldia"))); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("MotoresV2"), sqlOptions => sqlOptions.EnableRetryOnFailure())); // SERVICIOS builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure(builder.Configuration.GetSection("SmtpSettings")); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); // 馃敀 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(); // USAR EL MIDDLEWARE AL PRINCIPIO // Debe ser lo primero para que el RateLimiter y los Logs vean la IP real 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"); // 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();