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] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
[EnableRateLimiting("AuthPolicy")]
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
private readonly IIdentityService _identityService; private readonly IIdentityService _identityService;
@@ -28,12 +27,12 @@ public class AuthController : ControllerBase
} }
// Helper privado para cookies // Helper privado para cookies
private void SetTokenCookie(string token, string cookieName) private void SetTokenCookie(string token, string cookieName, DateTime expires)
{ {
var cookieOptions = new CookieOptions var cookieOptions = new CookieOptions
{ {
HttpOnly = true, // Seguridad: JS no puede leer esto 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) 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) 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 IsEssential = true
@@ -42,7 +41,7 @@ public class AuthController : ControllerBase
} }
[HttpPost("login")] [HttpPost("login")]
[EnableRateLimiting("AuthPolicy")] // PROTEGIDO (5 intentos/min) [EnableRateLimiting("AuthPolicy")]
public async Task<IActionResult> Login([FromBody] LoginRequest request) public async Task<IActionResult> Login([FromBody] LoginRequest request)
{ {
var (user, message) = await _identityService.AuthenticateAsync(request.Username, request.Password); var (user, message) = await _identityService.AuthenticateAsync(request.Username, request.Password);
@@ -89,8 +88,10 @@ public class AuthController : ControllerBase
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
// 3. Setear Cookies // 3. Setear Cookies
SetTokenCookie(jwtToken, "accessToken"); // El AccessToken dura 60 min (coincide con JWT)
SetTokenCookie(refreshToken.Token, "refreshToken"); 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 // 4. Audit Log
_context.AuditLogs.Add(new AuditLog _context.AuditLogs.Add(new AuditLog
@@ -122,7 +123,6 @@ public class AuthController : ControllerBase
} }
[HttpPost("refresh-token")] [HttpPost("refresh-token")]
// NO PROTEGIDO ESTRICTAMENTE (Usa límite global)
public async Task<IActionResult> RefreshToken() public async Task<IActionResult> RefreshToken()
{ {
var refreshToken = Request.Cookies["refreshToken"]; var refreshToken = Request.Cookies["refreshToken"];
@@ -154,14 +154,14 @@ public class AuthController : ControllerBase
var newJwtToken = _tokenService.GenerateJwtToken(user); var newJwtToken = _tokenService.GenerateJwtToken(user);
// Actualizar Cookies // Actualizar Cookies
SetTokenCookie(newJwtToken, "accessToken"); SetTokenCookie(newJwtToken, "accessToken", DateTime.UtcNow.AddMinutes(60));
SetTokenCookie(newRefreshToken.Token, "refreshToken"); // 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" }); return Ok(new { message = "Token renovado" });
} }
[HttpPost("logout")] [HttpPost("logout")]
// NO PROTEGIDO ESTRICTAMENTE
public IActionResult Logout() public IActionResult Logout()
{ {
Response.Cookies.Delete("accessToken"); Response.Cookies.Delete("accessToken");
@@ -287,8 +287,8 @@ public class AuthController : ControllerBase
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
// Setear Cookies Seguras // Setear Cookies Seguras
SetTokenCookie(token, "accessToken"); SetTokenCookie(token, "accessToken", DateTime.UtcNow.AddMinutes(60));
SetTokenCookie(refreshToken.Token, "refreshToken"); SetTokenCookie(refreshToken.Token, "refreshToken", DateTime.UtcNow.AddDays(7));
_context.AuditLogs.Add(new AuditLog _context.AuditLogs.Add(new AuditLog
{ {
@@ -386,7 +386,7 @@ public class AuthController : ControllerBase
} }
[HttpPost("register")] [HttpPost("register")]
[EnableRateLimiting("AuthPolicy")] // PROTEGIDO [EnableRateLimiting("AuthPolicy")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request) public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{ {
var (success, message) = await _identityService.RegisterUserAsync(request); var (success, message) = await _identityService.RegisterUserAsync(request);
@@ -407,7 +407,7 @@ public class AuthController : ControllerBase
} }
[HttpPost("verify-email")] [HttpPost("verify-email")]
[EnableRateLimiting("AuthPolicy")] // PROTEGIDO [EnableRateLimiting("AuthPolicy")]
public async Task<IActionResult> VerifyEmail([FromBody] VerifyEmailRequest request) public async Task<IActionResult> VerifyEmail([FromBody] VerifyEmailRequest request)
{ {
var (success, message) = await _identityService.VerifyEmailAsync(request.Token); var (success, message) = await _identityService.VerifyEmailAsync(request.Token);
@@ -428,7 +428,7 @@ public class AuthController : ControllerBase
} }
[HttpPost("resend-verification")] [HttpPost("resend-verification")]
[EnableRateLimiting("AuthPolicy")] // PROTEGIDO [EnableRateLimiting("AuthPolicy")]
public async Task<IActionResult> ResendVerification([FromBody] ResendVerificationRequest request) public async Task<IActionResult> ResendVerification([FromBody] ResendVerificationRequest request)
{ {
var (success, message) = await _identityService.ResendVerificationEmailAsync(request.Email); var (success, message) = await _identityService.ResendVerificationEmailAsync(request.Email);
@@ -437,7 +437,7 @@ public class AuthController : ControllerBase
} }
[HttpPost("forgot-password")] [HttpPost("forgot-password")]
[EnableRateLimiting("AuthPolicy")] // PROTEGIDO [EnableRateLimiting("AuthPolicy")]
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordRequest request) public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordRequest request)
{ {
var (success, message) = await _identityService.ForgotPasswordAsync(request.Email); var (success, message) = await _identityService.ForgotPasswordAsync(request.Email);
@@ -452,7 +452,7 @@ public class AuthController : ControllerBase
} }
[HttpPost("reset-password")] [HttpPost("reset-password")]
[EnableRateLimiting("AuthPolicy")] // PROTEGIDO [EnableRateLimiting("AuthPolicy")]
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordRequest request) public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordRequest request)
{ {
var (success, message) = await _identityService.ResetPasswordAsync(request.Token, request.NewPassword); var (success, message) = await _identityService.ResetPasswordAsync(request.Token, request.NewPassword);
@@ -474,7 +474,7 @@ public class AuthController : ControllerBase
[Authorize] [Authorize]
[HttpPost("change-password")] [HttpPost("change-password")]
[EnableRateLimiting("AuthPolicy")] // PROTEGIDO [EnableRateLimiting("AuthPolicy")]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request) public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{ {
var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); 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 // USAR EL MIDDLEWARE DE HEADERS
app.UseForwardedHeaders(); app.UseForwardedHeaders();
// 🔒 HEADERS DE SEGURIDAD MIDDLEWARE // 🔒 HEADERS DE SEGURIDAD & PNA FIX MIDDLEWARE
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
// --- 1. SEGURIDAD EXISTENTE (HARDENING) ---
context.Response.Headers.Append("X-Frame-Options", "DENY"); context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin"); 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("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Append("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"); context.Response.Headers.Append("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
string csp = "default-src 'self'; " + string csp = "default-src 'self'; " +
"img-src 'self' data: https: blob:; " + "img-src 'self' data: https: blob:; " +
"script-src 'self' 'unsafe-inline'; " + "script-src 'self' 'unsafe-inline'; " +
@@ -179,8 +181,32 @@ app.Use(async (context, next) =>
"form-action 'self'; " + "form-action 'self'; " +
"frame-ancestors 'none';"; "frame-ancestors 'none';";
context.Response.Headers.Append("Content-Security-Policy", csp); context.Response.Headers.Append("Content-Security-Policy", csp);
context.Response.Headers.Remove("Server"); context.Response.Headers.Remove("Server");
context.Response.Headers.Remove("X-Powered-By"); 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(); await next();
}); });

View File

@@ -279,6 +279,8 @@ public class IdentityService : IIdentityService
user.PasswordResetTokenExpiresAt = null; user.PasswordResetTokenExpiresAt = null;
user.PasswordSalt = null; user.PasswordSalt = null;
user.MigrationStatus = 1; user.MigrationStatus = 1;
user.IsEmailVerified = true;
user.VerificationToken = null;
await _v2Context.SaveChangesAsync(); await _v2Context.SaveChangesAsync();
return (true, "Tu contraseña ha sido actualizada correctamente."); return (true, "Tu contraseña ha sido actualizada correctamente.");

View File

@@ -131,7 +131,7 @@ export default function PremiumGallery({
{/* MINIATURAS (THUMBNAILS) */} {/* MINIATURAS (THUMBNAILS) */}
{photos.length > 1 && ( {photos.length > 1 && (
<div className="flex gap-3 md:gap-4 overflow-x-auto pt-4 pb-4 px-1 scrollbar-hide no-scrollbar items-center justify-center"> <div className="flex gap-3 md:gap-4 overflow-x-auto pt-4 pb-4 scrollbar-hide no-scrollbar items-center justify-start">
{photos.map((p, idx) => ( {photos.map((p, idx) => (
<button <button
key={idx} key={idx}

View File

@@ -1,5 +1,10 @@
import { createContext, useContext, useState, import {
useEffect, useCallback, type ReactNode, createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react"; } from "react";
import { AuthService, type UserSession } from "../services/auth.service"; import { AuthService, type UserSession } from "../services/auth.service";
import { ChatService } from "../services/chat.service"; import { ChatService } from "../services/chat.service";
@@ -50,19 +55,28 @@ export function AuthProvider({ children }: { children: ReactNode }) {
try { try {
const sessionUser = await AuthService.checkSession(); const sessionUser = await AuthService.checkSession();
if (sessionUser) { if (sessionUser) {
// Si la sesión es válida, actualizamos el estado si es necesario
// Comparamos IDs para evitar re-renders innecesarios si es el mismo usuario
if (sessionUser.id !== user?.id) { if (sessionUser.id !== user?.id) {
setUser(sessionUser); setUser(sessionUser);
} }
await fetchUnreadCount(); await fetchUnreadCount();
} else { } else {
// Si el backend dice null (sesión inválida), sacamos al usuario // El backend respondió 200 OK pero con body null (sesión inválida explícita)
if (user) logout(); if (user) logout();
} }
} catch (error) { } catch (error: any) {
// Si hay error de red o 401 que no se pudo refrescar // Solo desloguear si es un error 401 (No autorizado) o 403 (Prohibido).
if (user) logout(); // Si es un error de red (sin response) o 500, mantenemos al usuario "logueado" visualmente
// hasta que recupere conexión o intente una acción que falle.
if (
error.response &&
(error.response.status === 401 || error.response.status === 403)
) {
if (user) logout();
} else {
console.warn(
"No se pudo verificar la sesión (posible error de red), se mantiene estado actual.",
);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -86,7 +86,7 @@ export default function VehiculoDetailPage() {
const isOwnerAdmin = vehicle.ownerUserType === 3; const isOwnerAdmin = vehicle.ownerUserType === 3;
const isAdActive = vehicle.statusID === AD_STATUSES.ACTIVE; const isAdActive = vehicle.statusID === AD_STATUSES.ACTIVE;
const isContactable = isAdActive && !isOwnerAdmin; const isContactable = isAdActive && vehicle.displayContactInfo;
return ( return (
<div className="container mx-auto px-4 md:px-6 py-6 md:py-12 animate-fade-in-up"> <div className="container mx-auto px-4 md:px-6 py-6 md:py-12 animate-fade-in-up">