2026-01-29 13:43:44 -03:00
|
|
|
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
|
2026-03-12 13:52:33 -03:00
|
|
|
var frontendUrlConfig = builder.Configuration["AppSettings:FrontendUrl"] ?? "http://localhost:5173,https://clasificados.eldia.com";
|
|
|
|
|
var frontendUrls = frontendUrlConfig.Split(',');
|
2026-01-29 13:43:44 -03:00
|
|
|
builder.Services.AddCors(options =>
|
|
|
|
|
{
|
|
|
|
|
options.AddPolicy("AllowSpecificOrigin",
|
|
|
|
|
policy => policy.WithOrigins(frontendUrls)
|
|
|
|
|
.AllowAnyMethod()
|
|
|
|
|
.AllowAnyHeader()
|
|
|
|
|
.AllowCredentials());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// FORWARDED HEADERS (CRÍTICO PARA DOCKER/NGINX)
|
|
|
|
|
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 =>
|
|
|
|
|
{
|
|
|
|
|
var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
|
|
|
|
|
|
|
|
|
if (System.Net.IPAddress.IsLoopback(context.Connection.RemoteIpAddress!))
|
|
|
|
|
{
|
|
|
|
|
return RateLimitPartition.GetNoLimiter("loopback");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return RateLimitPartition.GetFixedWindowLimiter(
|
2026-02-12 11:07:43 -03:00
|
|
|
partitionKey: remoteIp,
|
2026-01-29 13:43:44 -03:00
|
|
|
factory: _ => new FixedWindowRateLimiterOptions
|
|
|
|
|
{
|
|
|
|
|
AutoReplenishment = true,
|
|
|
|
|
PermitLimit = 100,
|
|
|
|
|
QueueLimit = 2,
|
|
|
|
|
Window = TimeSpan.FromMinutes(1)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
options.AddPolicy("AuthPolicy", context =>
|
|
|
|
|
{
|
|
|
|
|
var remoteIp = context.Connection.RemoteIpAddress;
|
2026-01-30 11:18:56 -03:00
|
|
|
if (remoteIp != null && System.Net.IPAddress.IsLoopback(remoteIp))
|
2026-01-29 13:43:44 -03:00
|
|
|
{
|
|
|
|
|
return RateLimitPartition.GetNoLimiter("loopback_auth");
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 11:18:56 -03:00
|
|
|
return RateLimitPartition.GetFixedWindowLimiter("auth_limit", _ => new FixedWindowRateLimiterOptions
|
|
|
|
|
{
|
|
|
|
|
PermitLimit = 5,
|
|
|
|
|
Window = TimeSpan.FromMinutes(1),
|
|
|
|
|
QueueLimit = 0
|
|
|
|
|
});
|
2026-01-29 13:43:44 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-30 11:18:56 -03:00
|
|
|
// 🛡️ SEGURIDAD: Evitar que el host se caiga si un servicio de fondo falla
|
|
|
|
|
builder.Services.Configure<HostOptions>(options =>
|
|
|
|
|
{
|
|
|
|
|
options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-12 11:07:43 -03:00
|
|
|
// DB CONTEXTS
|
|
|
|
|
builder.Services.AddDbContext<InternetDbContext>(options =>
|
|
|
|
|
options.UseSqlServer(builder.Configuration.GetConnectionString("Internet")));
|
|
|
|
|
|
2026-01-29 13:43:44 -03:00
|
|
|
builder.Services.AddDbContext<MotoresV2DbContext>(options =>
|
|
|
|
|
options.UseSqlServer(builder.Configuration.GetConnectionString("MotoresV2"),
|
|
|
|
|
sqlOptions => sqlOptions.EnableRetryOnFailure()));
|
|
|
|
|
|
|
|
|
|
// SERVICIOS
|
|
|
|
|
builder.Services.AddScoped<IAvisosLegacyService, AvisosLegacyService>();
|
|
|
|
|
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>();
|
2026-03-12 13:52:33 -03:00
|
|
|
builder.Services.AddScoped<IAdSyncService, AdSyncService>();
|
|
|
|
|
builder.Services.AddScoped<INotificationService, NotificationService>();
|
|
|
|
|
builder.Services.AddScoped<INotificationPreferenceService, NotificationPreferenceService>();
|
|
|
|
|
builder.Services.AddScoped<ITokenService, TokenService>();
|
2026-01-29 13:43:44 -03:00
|
|
|
builder.Services.Configure<MailSettings>(builder.Configuration.GetSection("SmtpSettings"));
|
|
|
|
|
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
|
|
|
|
builder.Services.AddScoped<IImageStorageService, ImageStorageService>();
|
|
|
|
|
builder.Services.AddHostedService<AdExpirationService>();
|
2026-01-30 11:18:56 -03:00
|
|
|
builder.Services.AddHostedService<TokenCleanupService>();
|
2026-03-21 20:11:50 -03:00
|
|
|
builder.Services.AddHostedService<SitemapGeneratorService>();
|
2026-01-29 13:43:44 -03:00
|
|
|
|
|
|
|
|
// 🔒 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 =>
|
|
|
|
|
{
|
|
|
|
|
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();
|
|
|
|
|
|
2026-01-30 11:18:56 -03:00
|
|
|
// Middleware de Manejo Global de Excepciones (Debe ser el primero para atrapar todo)
|
|
|
|
|
app.UseMiddleware<MotoresArgentinosV2.API.Middleware.ExceptionHandlingMiddleware>();
|
|
|
|
|
|
|
|
|
|
// USAR EL MIDDLEWARE DE HEADERS
|
2026-01-29 13:43:44 -03:00
|
|
|
app.UseForwardedHeaders();
|
|
|
|
|
|
2026-02-18 21:00:35 -03:00
|
|
|
// 🔒 HEADERS DE SEGURIDAD & PNA FIX MIDDLEWARE
|
2026-01-29 13:43:44 -03:00
|
|
|
app.Use(async (context, next) =>
|
|
|
|
|
{
|
2026-02-18 21:00:35 -03:00
|
|
|
// --- 1. SEGURIDAD EXISTENTE (HARDENING) ---
|
2026-01-29 13:43:44 -03:00
|
|
|
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");
|
2026-01-30 11:18:56 -03:00
|
|
|
context.Response.Headers.Append("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
|
2026-02-18 21:00:35 -03:00
|
|
|
|
2026-01-29 13:43:44 -03:00
|
|
|
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'; " +
|
2026-02-13 15:07:16 -03:00
|
|
|
"form-action 'self'; " +
|
2026-01-29 13:43:44 -03:00
|
|
|
"frame-ancestors 'none';";
|
|
|
|
|
context.Response.Headers.Append("Content-Security-Policy", csp);
|
2026-02-18 21:00:35 -03:00
|
|
|
|
2026-01-29 13:43:44 -03:00
|
|
|
context.Response.Headers.Remove("Server");
|
|
|
|
|
context.Response.Headers.Remove("X-Powered-By");
|
2026-02-18 21:00:35 -03:00
|
|
|
|
|
|
|
|
// Esto permite que el sitio público (eldia.com) pida recursos a tu IP local/privada.
|
|
|
|
|
// Si el navegador pregunta explícitamente "Puedo acceder a la red privada?"
|
|
|
|
|
if (context.Request.Headers.ContainsKey("Access-Control-Request-Private-Network"))
|
|
|
|
|
{
|
|
|
|
|
context.Response.Headers.Append("Access-Control-Allow-Private-Network", "true");
|
|
|
|
|
}
|
|
|
|
|
// O si estamos sirviendo imágenes/API (Backup por si el navegador no manda el header de request)
|
|
|
|
|
else if (context.Request.Path.StartsWithSegments("/uploads") || context.Request.Path.StartsWithSegments("/api"))
|
|
|
|
|
{
|
|
|
|
|
context.Response.Headers.Append("Access-Control-Allow-Private-Network", "true");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Asegurar que el header esté presente en las peticiones OPTIONS (Preflight)
|
|
|
|
|
if (context.Request.Method == "OPTIONS")
|
|
|
|
|
{
|
|
|
|
|
// A veces es necesario forzarlo aquí también para que el preflight pase
|
|
|
|
|
if (!context.Response.Headers.ContainsKey("Access-Control-Allow-Private-Network"))
|
|
|
|
|
{
|
|
|
|
|
context.Response.Headers.Append("Access-Control-Allow-Private-Network", "true");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 13:43:44 -03:00
|
|
|
await next();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// PIPELINE
|
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
|
|
|
{
|
|
|
|
|
app.UseSwagger();
|
|
|
|
|
app.UseSwaggerUI();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
app.UseHsts();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.UseHttpsRedirection();
|
|
|
|
|
app.UseStaticFiles();
|
|
|
|
|
|
|
|
|
|
// 🔒 APLICAR CORS & RATE LIMIT
|
2026-02-13 15:07:16 -03:00
|
|
|
app.Use(async (context, next) =>
|
|
|
|
|
{
|
|
|
|
|
// Para las peticiones de imágenes, agregamos el header PNA
|
|
|
|
|
if (context.Request.Path.StartsWithSegments("/uploads"))
|
|
|
|
|
{
|
|
|
|
|
context.Response.Headers.Append("Access-Control-Allow-Private-Network", "true");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await next();
|
|
|
|
|
});
|
2026-01-29 13:43:44 -03:00
|
|
|
app.UseCors("AllowSpecificOrigin");
|
|
|
|
|
app.UseRateLimiter();
|
|
|
|
|
|
|
|
|
|
app.UseAuthentication();
|
|
|
|
|
app.UseAuthorization();
|
|
|
|
|
|
|
|
|
|
app.MapControllers();
|
|
|
|
|
|
|
|
|
|
app.Run();
|