diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs b/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs index 2a782fd..c4defc8 100644 --- a/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs +++ b/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs @@ -11,7 +11,6 @@ namespace MotoresArgentinosV2.API.Controllers; [ApiController] [Route("api/[controller]")] -[EnableRateLimiting("AuthPolicy")] public class AuthController : ControllerBase { private readonly IIdentityService _identityService; @@ -28,12 +27,12 @@ public class AuthController : ControllerBase } // Helper privado para cookies - private void SetTokenCookie(string token, string cookieName) + private void SetTokenCookie(string token, string cookieName, DateTime expires) { var cookieOptions = new CookieOptions { HttpOnly = true, // Seguridad: JS no puede leer esto - Expires = DateTime.UtcNow.AddMinutes(15), + Expires = expires, Secure = true, // Solo HTTPS (Para tests locales 'Secure = false' temporalmente) SameSite = SameSiteMode.Strict, // Protección CSRF (Strict para máxima seguridad, pero puede ser Lax si hay problemas con redirecciones y testeos locales) IsEssential = true @@ -42,7 +41,7 @@ public class AuthController : ControllerBase } [HttpPost("login")] - [EnableRateLimiting("AuthPolicy")] // PROTEGIDO (5 intentos/min) + [EnableRateLimiting("AuthPolicy")] public async Task Login([FromBody] LoginRequest request) { var (user, message) = await _identityService.AuthenticateAsync(request.Username, request.Password); @@ -89,8 +88,10 @@ public class AuthController : ControllerBase await _context.SaveChangesAsync(); // 3. Setear Cookies - SetTokenCookie(jwtToken, "accessToken"); - SetTokenCookie(refreshToken.Token, "refreshToken"); + // El AccessToken dura 60 min (coincide con JWT) + SetTokenCookie(jwtToken, "accessToken", DateTime.UtcNow.AddMinutes(60)); + // El RefreshToken dura 7 días (coincide con DB) + SetTokenCookie(refreshToken.Token, "refreshToken", DateTime.UtcNow.AddDays(7)); // 4. Audit Log _context.AuditLogs.Add(new AuditLog @@ -122,7 +123,6 @@ public class AuthController : ControllerBase } [HttpPost("refresh-token")] - // NO PROTEGIDO ESTRICTAMENTE (Usa límite global) public async Task RefreshToken() { var refreshToken = Request.Cookies["refreshToken"]; @@ -154,14 +154,14 @@ public class AuthController : ControllerBase var newJwtToken = _tokenService.GenerateJwtToken(user); // Actualizar Cookies - SetTokenCookie(newJwtToken, "accessToken"); - SetTokenCookie(newRefreshToken.Token, "refreshToken"); + SetTokenCookie(newJwtToken, "accessToken", DateTime.UtcNow.AddMinutes(60)); + // El refresh token DEBE durar 7 días para mantener la sesión viva + SetTokenCookie(newRefreshToken.Token, "refreshToken", DateTime.UtcNow.AddDays(7)); return Ok(new { message = "Token renovado" }); } [HttpPost("logout")] - // NO PROTEGIDO ESTRICTAMENTE public IActionResult Logout() { Response.Cookies.Delete("accessToken"); @@ -287,8 +287,8 @@ public class AuthController : ControllerBase await _context.SaveChangesAsync(); // Setear Cookies Seguras - SetTokenCookie(token, "accessToken"); - SetTokenCookie(refreshToken.Token, "refreshToken"); + SetTokenCookie(token, "accessToken", DateTime.UtcNow.AddMinutes(60)); + SetTokenCookie(refreshToken.Token, "refreshToken", DateTime.UtcNow.AddDays(7)); _context.AuditLogs.Add(new AuditLog { @@ -386,7 +386,7 @@ public class AuthController : ControllerBase } [HttpPost("register")] - [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + [EnableRateLimiting("AuthPolicy")] public async Task Register([FromBody] RegisterRequest request) { var (success, message) = await _identityService.RegisterUserAsync(request); @@ -407,7 +407,7 @@ public class AuthController : ControllerBase } [HttpPost("verify-email")] - [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + [EnableRateLimiting("AuthPolicy")] public async Task VerifyEmail([FromBody] VerifyEmailRequest request) { var (success, message) = await _identityService.VerifyEmailAsync(request.Token); @@ -428,7 +428,7 @@ public class AuthController : ControllerBase } [HttpPost("resend-verification")] - [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + [EnableRateLimiting("AuthPolicy")] public async Task ResendVerification([FromBody] ResendVerificationRequest request) { var (success, message) = await _identityService.ResendVerificationEmailAsync(request.Email); @@ -437,7 +437,7 @@ public class AuthController : ControllerBase } [HttpPost("forgot-password")] - [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + [EnableRateLimiting("AuthPolicy")] public async Task ForgotPassword([FromBody] ForgotPasswordRequest request) { var (success, message) = await _identityService.ForgotPasswordAsync(request.Email); @@ -452,7 +452,7 @@ public class AuthController : ControllerBase } [HttpPost("reset-password")] - [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + [EnableRateLimiting("AuthPolicy")] public async Task ResetPassword([FromBody] ResetPasswordRequest request) { var (success, message) = await _identityService.ResetPasswordAsync(request.Token, request.NewPassword); @@ -474,7 +474,7 @@ public class AuthController : ControllerBase [Authorize] [HttpPost("change-password")] - [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + [EnableRateLimiting("AuthPolicy")] public async Task ChangePassword([FromBody] ChangePasswordRequest request) { var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); diff --git a/Backend/MotoresArgentinosV2.API/Program.cs b/Backend/MotoresArgentinosV2.API/Program.cs index dd2b6b3..7a48747 100644 --- a/Backend/MotoresArgentinosV2.API/Program.cs +++ b/Backend/MotoresArgentinosV2.API/Program.cs @@ -161,14 +161,16 @@ app.UseMiddleware { + // --- 1. SEGURIDAD EXISTENTE (HARDENING) --- 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"); context.Response.Headers.Append("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"); + string csp = "default-src 'self'; " + "img-src 'self' data: https: blob:; " + "script-src 'self' 'unsafe-inline'; " + @@ -179,8 +181,32 @@ app.Use(async (context, next) => "form-action 'self'; " + "frame-ancestors 'none';"; context.Response.Headers.Append("Content-Security-Policy", csp); + context.Response.Headers.Remove("Server"); context.Response.Headers.Remove("X-Powered-By"); + + // 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"); + } + } + await next(); }); diff --git a/Backend/MotoresArgentinosV2.Infrastructure/Services/IdentityService.cs b/Backend/MotoresArgentinosV2.Infrastructure/Services/IdentityService.cs index fd29cc3..d44cc96 100644 --- a/Backend/MotoresArgentinosV2.Infrastructure/Services/IdentityService.cs +++ b/Backend/MotoresArgentinosV2.Infrastructure/Services/IdentityService.cs @@ -279,6 +279,8 @@ public class IdentityService : IIdentityService user.PasswordResetTokenExpiresAt = null; user.PasswordSalt = null; user.MigrationStatus = 1; + user.IsEmailVerified = true; + user.VerificationToken = null; await _v2Context.SaveChangesAsync(); return (true, "Tu contraseña ha sido actualizada correctamente."); diff --git a/Frontend/src/components/PremiumGallery.tsx b/Frontend/src/components/PremiumGallery.tsx index 6f9ec37..a747d29 100644 --- a/Frontend/src/components/PremiumGallery.tsx +++ b/Frontend/src/components/PremiumGallery.tsx @@ -131,7 +131,7 @@ export default function PremiumGallery({ {/* MINIATURAS (THUMBNAILS) */} {photos.length > 1 && ( -
+
{photos.map((p, idx) => (