Fix: Ajustes UI/UX y Seguridad Producción.

This commit is contained in:
2026-02-13 11:23:16 -03:00
parent e096ed1590
commit ed243ccb78
3 changed files with 634 additions and 247 deletions

View File

@@ -34,8 +34,8 @@ public class AuthController : ControllerBase
{ {
HttpOnly = true, // Seguridad: JS no puede leer esto HttpOnly = true, // Seguridad: JS no puede leer esto
Expires = DateTime.UtcNow.AddMinutes(15), Expires = DateTime.UtcNow.AddMinutes(15),
Secure = false, // Solo HTTPS (Para tests locales 'Secure = false' temporalmente) Secure = true, // Solo HTTPS (Para tests locales 'Secure = false' temporalmente)
SameSite = SameSiteMode.Lax, // 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
}; };
Response.Cookies.Append(cookieName, token, cookieOptions); Response.Cookies.Append(cookieName, token, cookieOptions);

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,42 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from 'react-router-dom'; // Importar useNavigate import { useSearchParams, useNavigate } from "react-router-dom";
import { AvisosService } from '../services/avisos.service'; import { AvisosService } from "../services/avisos.service";
import { AdsV2Service } from '../services/ads.v2.service'; import { AdsV2Service } from "../services/ads.v2.service";
import { AuthService, type UserSession } from '../services/auth.service'; import { AuthService, type UserSession } from "../services/auth.service";
import type { DatosAvisoDto } from '../types/aviso.types'; import type { DatosAvisoDto } from "../types/aviso.types";
import FormularioAviso from '../components/FormularioAviso'; import FormularioAviso from "../components/FormularioAviso";
import LoginModal from '../components/LoginModal'; import LoginModal from "../components/LoginModal";
const TAREAS_DISPONIBLES = [ const TAREAS_DISPONIBLES = [
{ id: 'EAUTOS', label: 'Automóviles', icon: '🚗', description: 'Venta de Autos, Camionetas y Utilitarios' }, {
{ id: 'EMOTOS', label: 'Motos', icon: '🏍️', description: 'Venta de Motos, Cuatriciclos y Náutica' }, id: "EAUTOS",
label: "Automóviles",
icon: "🚗",
description: "Venta de Autos, Camionetas y Utilitarios",
},
{
id: "EMOTOS",
label: "Motos",
icon: "🏍️",
description: "Venta de Motos, Cuatriciclos y Náutica",
},
]; ];
export default function PublicarAvisoPage() { export default function PublicarAvisoPage() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate(); // Hook de navegación const navigate = useNavigate(); // Hook de navegación
const editId = searchParams.get('edit'); const editId = searchParams.get("edit");
const [categorySelection, setCategorySelection] = useState<string>(''); const [categorySelection, setCategorySelection] = useState<string>("");
const [tarifas, setTarifas] = useState<DatosAvisoDto[]>([]); const [tarifas, setTarifas] = useState<DatosAvisoDto[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [planSeleccionado, setPlanSeleccionado] = useState<DatosAvisoDto | null>(null); const [planSeleccionado, setPlanSeleccionado] =
useState<DatosAvisoDto | null>(null);
const [fixedCategory, setFixedCategory] = useState<string | null>(null); const [fixedCategory, setFixedCategory] = useState<string | null>(null);
const [user, setUser] = useState<UserSession | null>(AuthService.getCurrentUser()); const [user, setUser] = useState<UserSession | null>(
AuthService.getCurrentUser(),
);
useEffect(() => { useEffect(() => {
if (editId) { if (editId) {
@@ -31,30 +44,24 @@ export default function PublicarAvisoPage() {
} }
}, [editId]); }, [editId]);
useEffect(() => {
if (planSeleccionado) {
window.scrollTo({ top: 0, behavior: "instant" });
}
}, [planSeleccionado]);
const cargarAvisoParaEdicion = async (id: number) => { const cargarAvisoParaEdicion = async (id: number) => {
setLoading(true); setLoading(true);
try { try {
const ad = await AdsV2Service.getById(id); const ad = await AdsV2Service.getById(id);
// Determinamos la categoría para cargar las tarifas correspondientes // Determinamos la categoría para cargar las tarifas correspondientes
const categoryCode = ad.vehicleTypeID === 1 ? 'EAUTOS' : 'EMOTOS'; const categoryCode = ad.vehicleTypeID === 1 ? "EAUTOS" : "EMOTOS";
setCategorySelection(categoryCode); setCategorySelection(categoryCode);
// 🟢 FIX: Bloquear el cambio de categoría // Bloquear el cambio de categoría
setFixedCategory(categoryCode); setFixedCategory(categoryCode);
// 🟢 FIX: NO seleccionamos plan automáticamente.
// Dejamos que el usuario elija el plan en las tarjetas.
// (Eliminamos todo el bloque de setPlanSeleccionado)
/* BLOQUE ELIMINADO:
const tarifasData = await AvisosService.obtenerConfiguracion('EMOTORES', ad.isFeatured ? 1 : 0);
const tarifaReal = tarifasData[0];
if (!tarifaReal) throw new Error("Tarifa no encontrada");
setPlanSeleccionado({ ... });
*/
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setError("Error al cargar el aviso."); setError("Error al cargar el aviso.");
@@ -71,8 +78,8 @@ export default function PublicarAvisoPage() {
setError(null); setError(null);
try { try {
const [simple, destacado] = await Promise.all([ const [simple, destacado] = await Promise.all([
AvisosService.obtenerConfiguracion('EMOTORES', 0), AvisosService.obtenerConfiguracion("EMOTORES", 0),
AvisosService.obtenerConfiguracion('EMOTORES', 1) AvisosService.obtenerConfiguracion("EMOTORES", 1),
]); ]);
const planes = [...simple, ...destacado]; const planes = [...simple, ...destacado];
@@ -90,26 +97,30 @@ export default function PublicarAvisoPage() {
}, [categorySelection]); }, [categorySelection]);
const handleSelectPlan = (plan: DatosAvisoDto) => { const handleSelectPlan = (plan: DatosAvisoDto) => {
const vehicleTypeId = categorySelection === 'EAUTOS' ? 1 : 2; const vehicleTypeId = categorySelection === "EAUTOS" ? 1 : 2;
const nombrePlanAmigable = plan.paquete === 1 ? 'PLAN DESTACADO' : 'PLAN ESTÁNDAR'; const nombrePlanAmigable =
plan.paquete === 1 ? "PLAN DESTACADO" : "PLAN ESTÁNDAR";
setPlanSeleccionado({ setPlanSeleccionado({
...plan, ...plan,
idRubro: vehicleTypeId, idRubro: vehicleTypeId,
nomavi: nombrePlanAmigable nomavi: nombrePlanAmigable,
}); });
}; };
// Manejador centralizado de éxito // Manejador centralizado de éxito
const handleSuccess = (adId: number, isAdminAction: boolean = false) => { const handleSuccess = (adId: number, isAdminAction: boolean = false) => {
const status = isAdminAction ? 'admin_created' : 'approved'; const status = isAdminAction ? "admin_created" : "approved";
navigate(`/pago-confirmado?status=${status}&adId=${adId}`); navigate(`/pago-confirmado?status=${status}&adId=${adId}`);
}; };
if (!user) { if (!user) {
return ( return (
<div className="flex justify-center items-center py-20 min-h-[60vh]"> <div className="flex justify-center items-center py-20 min-h-[60vh]">
<LoginModal onSuccess={(u) => setUser(u)} onClose={() => navigate('/')} /> <LoginModal
onSuccess={(u) => setUser(u)}
onClose={() => navigate("/")}
/>
</div> </div>
); );
} }
@@ -120,11 +131,15 @@ export default function PublicarAvisoPage() {
return ( return (
<div className="max-w-6xl mx-auto py-8 px-6"> <div className="max-w-6xl mx-auto py-8 px-6">
<header className="flex justify-between items-center mb-10"> <header className="flex justify-between items-center mb-10">
<button onClick={() => setPlanSeleccionado(null)} className="text-gray-500 hover:text-white uppercase text-[10px] font-black tracking-widest flex items-center gap-2 transition-colors"> <button
onClick={() => setPlanSeleccionado(null)}
className="text-gray-500 hover:text-white uppercase text-[10px] font-black tracking-widest flex items-center gap-2 transition-colors"
>
Volver a Planes Volver a Planes
</button> </button>
<div className="glass px-4 py-2 rounded-xl text-xs border border-white/5"> <div className="glass px-4 py-2 rounded-xl text-xs border border-white/5">
Publicando como: <span className="text-blue-400 font-bold">{user.username}</span> Publicando como:{" "}
<span className="text-blue-400 font-bold">{user.username}</span>
</div> </div>
</header> </header>
<FormularioAviso <FormularioAviso
@@ -140,13 +155,17 @@ export default function PublicarAvisoPage() {
return ( return (
<div className="max-w-6xl mx-auto px-4 md:px-6 py-8 md:py-12"> <div className="max-w-6xl mx-auto px-4 md:px-6 py-8 md:py-12">
<header className="mb-8 md:mb-16 text-center md:text-left"> <header className="mb-8 md:mb-16 text-center md:text-left">
<h2 className="text-3xl md:text-6xl font-black tracking-tighter uppercase mb-2">Comienza a <span className="text-gradient">Vender</span></h2> <h2 className="text-3xl md:text-6xl font-black tracking-tighter uppercase mb-2">
<p className="text-gray-500 text-sm md:text-lg italic">Selecciona una categoría para ver los planes de publicación.</p> Comienza a <span className="text-gradient">Vender</span>
</h2>
<p className="text-gray-500 text-sm md:text-lg italic">
Selecciona una categoría para ver los planes de publicación.
</p>
</header> </header>
{/* SECCIÓN DE CATEGORÍA */} {/* SECCIÓN DE CATEGORÍA */}
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 mb-10 md:mb-20 text-white"> <section className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 mb-10 md:mb-20 text-white">
{TAREAS_DISPONIBLES.map(t => { {TAREAS_DISPONIBLES.map((t) => {
// Lógica de bloqueo // Lógica de bloqueo
const isDisabled = fixedCategory && fixedCategory !== t.id; const isDisabled = fixedCategory && fixedCategory !== t.id;
@@ -157,16 +176,24 @@ export default function PublicarAvisoPage() {
disabled={!!isDisabled} // Deshabilitar botón disabled={!!isDisabled} // Deshabilitar botón
className={` className={`
glass-card p-6 md:p-10 rounded-[2rem] md:rounded-[2.5rem] flex items-center justify-between group transition-all text-left glass-card p-6 md:p-10 rounded-[2rem] md:rounded-[2.5rem] flex items-center justify-between group transition-all text-left
${categorySelection === t.id ? 'border-blue-500 scale-[1.02] shadow-2xl shadow-blue-600/10 bg-white/5' : 'hover:bg-white/5'} ${categorySelection === t.id ? "border-blue-500 scale-[1.02] shadow-2xl shadow-blue-600/10 bg-white/5" : "hover:bg-white/5"}
${isDisabled ? 'opacity-30 cursor-not-allowed grayscale' : 'cursor-pointer'} ${isDisabled ? "opacity-30 cursor-not-allowed grayscale" : "cursor-pointer"}
`} `}
> >
<div> <div>
<span className="text-[10px] md:text-xs font-black uppercase tracking-widest text-blue-400 mb-1 md:mb-2 block">Categoría</span> <span className="text-[10px] md:text-xs font-black uppercase tracking-widest text-blue-400 mb-1 md:mb-2 block">
<h3 className="text-2xl md:text-4xl font-bold mb-1 md:mb-2 uppercase tracking-tight">{t.label}</h3> Categoría
<p className="text-gray-500 font-light text-xs md:text-sm">{t.description}</p> </span>
<h3 className="text-2xl md:text-4xl font-bold mb-1 md:mb-2 uppercase tracking-tight">
{t.label}
</h3>
<p className="text-gray-500 font-light text-xs md:text-sm">
{t.description}
</p>
</div> </div>
<span className="text-4xl md:text-6xl group-hover:scale-110 transition-transform duration-300">{t.icon}</span> <span className="text-4xl md:text-6xl group-hover:scale-110 transition-transform duration-300">
{t.icon}
</span>
</button> </button>
); );
})} })}
@@ -177,25 +204,36 @@ export default function PublicarAvisoPage() {
<section className="animate-fade-in-up"> <section className="animate-fade-in-up">
<div className="flex justify-between items-end mb-10 border-b border-white/5 pb-4"> <div className="flex justify-between items-end mb-10 border-b border-white/5 pb-4">
<div> <div>
<h3 className="text-3xl font-black uppercase tracking-tighter">Planes <span className="text-blue-400">Disponibles</span></h3> <h3 className="text-3xl font-black uppercase tracking-tighter">
<p className="text-gray-500 text-xs mt-1 uppercase tracking-widest">Para {categorySelection === 'EAUTOS' ? 'Automóviles' : 'Motos'}</p> Planes <span className="text-blue-400">Disponibles</span>
</h3>
<p className="text-gray-500 text-xs mt-1 uppercase tracking-widest">
Para {categorySelection === "EAUTOS" ? "Automóviles" : "Motos"}
</p>
</div> </div>
<span className="text-gray-600 text-[10px] font-bold uppercase tracking-widest">Precios finales (IVA Incluido)</span> <span className="text-gray-600 text-[10px] font-bold uppercase tracking-widest">
Precios finales (IVA Incluido)
</span>
</div> </div>
{error && ( {error && (
<div className="bg-red-500/10 text-red-400 p-6 rounded-[2rem] border border-red-500/20 mb-10 text-center"> <div className="bg-red-500/10 text-red-400 p-6 rounded-[2rem] border border-red-500/20 mb-10 text-center">
<p className="font-bold uppercase tracking-widest text-xs mb-2">Error de Conexión</p> <p className="font-bold uppercase tracking-widest text-xs mb-2">
Error de Conexión
</p>
<p className="italic text-sm">{error}</p> <p className="italic text-sm">{error}</p>
</div> </div>
)} )}
{loading ? ( {loading ? (
<div className="flex justify-center p-20"><div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div></div> <div className="flex justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{tarifas.map((tarifa, idx) => { {tarifas.map((tarifa, idx) => {
const precioRaw = tarifa.importeTotsiniva > 0 const precioRaw =
tarifa.importeTotsiniva > 0
? tarifa.importeTotsiniva * 1.105 ? tarifa.importeTotsiniva * 1.105
: tarifa.importeSiniva * 1.105; : tarifa.importeSiniva * 1.105;
const precioFinal = Math.round(precioRaw); const precioFinal = Math.round(precioRaw);
@@ -206,14 +244,20 @@ export default function PublicarAvisoPage() {
: "Presencia esencial. Tu aviso aparecerá en el listado general de búsqueda."; : "Presencia esencial. Tu aviso aparecerá en el listado general de búsqueda.";
return ( return (
<div key={idx} onClick={() => handleSelectPlan(tarifa)} <div
className={`glass-card p-8 rounded-[2.5rem] flex flex-col group cursor-pointer relative overflow-hidden transition-all hover:-translate-y-2 hover:shadow-2xl ${esDestacado ? 'border-blue-500/30 hover:border-blue-500 hover:shadow-blue-900/20' : 'hover:border-white/30'}`}> key={idx}
onClick={() => handleSelectPlan(tarifa)}
<div className={`absolute top-0 right-0 text-white text-[9px] font-black px-6 py-2 rounded-bl-3xl uppercase tracking-widest shadow-lg ${esDestacado ? 'bg-gradient-to-bl from-blue-600 to-cyan-500 animate-glow' : 'bg-white/10 text-gray-400'}`}> className={`glass-card p-8 rounded-[2.5rem] flex flex-col group cursor-pointer relative overflow-hidden transition-all hover:-translate-y-2 hover:shadow-2xl ${esDestacado ? "border-blue-500/30 hover:border-blue-500 hover:shadow-blue-900/20" : "hover:border-white/30"}`}
{esDestacado ? 'RECOMENDADO' : 'BÁSICO'} >
<div
className={`absolute top-0 right-0 text-white text-[9px] font-black px-6 py-2 rounded-bl-3xl uppercase tracking-widest shadow-lg ${esDestacado ? "bg-gradient-to-bl from-blue-600 to-cyan-500 animate-glow" : "bg-white/10 text-gray-400"}`}
>
{esDestacado ? "RECOMENDADO" : "BÁSICO"}
</div> </div>
<h4 className={`text-3xl font-black uppercase tracking-tighter mb-4 mt-4 ${esDestacado ? 'text-blue-400' : 'text-white'}`}> <h4
className={`text-3xl font-black uppercase tracking-tighter mb-4 mt-4 ${esDestacado ? "text-blue-400" : "text-white"}`}
>
{tituloPlan} {tituloPlan}
</h4> </h4>
@@ -224,33 +268,61 @@ export default function PublicarAvisoPage() {
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex justify-between text-xs text-gray-300 items-center"> <li className="flex justify-between text-xs text-gray-300 items-center">
<span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">Plataforma</span> <span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">
<span className="font-bold bg-white/5 px-2 py-1 rounded text-[10px]">SOLO WEB</span> Plataforma
</span>
<span className="font-bold bg-white/5 px-2 py-1 rounded text-[10px]">
SOLO WEB
</span>
</li> </li>
<li className="flex justify-between text-xs text-gray-300 items-center"> <li className="flex justify-between text-xs text-gray-300 items-center">
<span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">Duración</span> <span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">
<span className="font-bold">{tarifa.cantidadDias} Días</span> Duración
</span>
<span className="font-bold">
{tarifa.cantidadDias} Días
</span>
</li> </li>
<li className="flex justify-between text-xs text-gray-300 items-center"> <li className="flex justify-between text-xs text-gray-300 items-center">
<span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">Fotos</span> <span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">
<span className="font-bold text-green-400">Hasta 5</span> Fotos
</span>
<span className="font-bold text-green-400">
Hasta 5
</span>
</li> </li>
<li className="flex justify-between text-xs text-gray-300 items-center"> <li className="flex justify-between text-xs text-gray-300 items-center">
<span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">Visibilidad</span> <span className="text-gray-500 uppercase tracking-wider font-bold text-[10px]">
<span className={`font-bold ${esDestacado ? 'text-blue-400' : 'text-gray-300'}`}>{esDestacado ? 'ALTA ⭐' : 'NORMAL'}</span> Visibilidad
</span>
<span
className={`font-bold ${esDestacado ? "text-blue-400" : "text-gray-300"}`}
>
{esDestacado ? "ALTA ⭐" : "NORMAL"}
</span>
</li> </li>
</ul> </ul>
</div> </div>
<div className="mt-auto pt-6 border-t border-white/5"> <div className="mt-auto pt-6 border-t border-white/5">
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest block mb-1">Precio Final</span> <span className="text-gray-500 text-[10px] font-black uppercase tracking-widest block mb-1">
Precio Final
</span>
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<span className="text-4xl font-black tracking-tighter text-white"> <span className="text-4xl font-black tracking-tighter text-white">
${precioFinal.toLocaleString('es-AR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} $
{precioFinal.toLocaleString("es-AR", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})}
</span>
<span className="text-xs font-bold text-gray-500">
ARS
</span> </span>
<span className="text-xs font-bold text-gray-500">ARS</span>
</div> </div>
<button className={`w-full mt-6 text-white py-4 rounded-xl font-bold uppercase text-xs tracking-widest transition-all shadow-lg ${esDestacado ? 'bg-blue-600 hover:bg-blue-500 shadow-blue-600/20' : 'bg-white/5 hover:bg-white/10'}`}> <button
className={`w-full mt-6 text-white py-4 rounded-xl font-bold uppercase text-xs tracking-widest transition-all shadow-lg ${esDestacado ? "bg-blue-600 hover:bg-blue-500 shadow-blue-600/20" : "bg-white/5 hover:bg-white/10"}`}
>
Seleccionar Seleccionar
</button> </button>
</div> </div>