Fix: Galeria Movil, Contactos, Estado de Verificación de Mail al Cambiar Clave y Otros.

This commit is contained in:
2026-02-18 21:00:35 -03:00
parent 5a7c3f62f1
commit ba9b0b3547
6 changed files with 71 additions and 29 deletions

View File

@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{
var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0");

View File

@@ -161,14 +161,16 @@ app.UseMiddleware<MotoresArgentinosV2.API.Middleware.ExceptionHandlingMiddleware
// USAR EL MIDDLEWARE DE HEADERS
app.UseForwardedHeaders();
// 🔒 HEADERS DE SEGURIDAD MIDDLEWARE
// 🔒 HEADERS DE SEGURIDAD & PNA FIX MIDDLEWARE
app.Use(async (context, next) =>
{
// --- 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();
});