Fix: Galeria Movil, Contactos, Estado de Verificación de Mail al Cambiar Clave y Otros.
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user