fix(perfil): detalles senalados en la revision
Revisión con IA / ai-review (pull_request) Successful in 24s

- verificarSesion acepta el rol esperado. Las tres sesiones (admin, cliente,
  jefe) se firman con el mismo secreto, asi que un token de jefe puesto en la
  cookie cliente_session pasaba la verificacion de firma; ahora el rol se
  comprueba. Verificado: con un JWT de jefe, /api/cliente/perfil da 401.
- El update traduce el P2002 de los indices unicos a un 409 legible, por si otra
  request toma el mismo email o telefono entre la comprobacion y el update.
- La pantalla ya no queda en "Cargando..." para siempre si falla el pedido.
- Si la sesion vence mientras edita, el PATCH manda a entrar de nuevo en vez de
  mostrar un error suelto.

Queda como mejora aparte pasarle el rol esperado al resto de las rutas del area
del cliente, que hoy solo validan la firma.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Beldor
2026-08-05 16:09:40 -03:00
parent 62dab5e025
commit ce7063514c
3 changed files with 43 additions and 15 deletions
+26 -13
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { verificarSesion } from "@/lib/auth-admin"; import { verificarSesion } from "@/lib/auth-admin";
import { aFormatoWhatsapp, aLocalArgentino, esLocalArgentinoValido } from "@/lib/telefono"; import { aFormatoWhatsapp, aLocalArgentino, esLocalArgentinoValido } from "@/lib/telefono";
@@ -8,7 +9,7 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// Datos de contacto del cliente logueado. El teléfono se devuelve en formato local // Datos de contacto del cliente logueado. El teléfono se devuelve en formato local
// (sin el +54 9) porque el formulario lo muestra como prefijo fijo. // (sin el +54 9) porque el formulario lo muestra como prefijo fijo.
export async function GET() { export async function GET() {
const sesion = await verificarSesion("cliente_session"); const sesion = await verificarSesion("cliente_session", "cliente");
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 }); if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
const cliente = await prisma.cliente.findUnique({ const cliente = await prisma.cliente.findUnique({
@@ -26,7 +27,7 @@ export async function GET() {
} }
export async function PATCH(req: NextRequest) { export async function PATCH(req: NextRequest) {
const sesion = await verificarSesion("cliente_session"); const sesion = await verificarSesion("cliente_session", "cliente");
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 }); if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
const { nombre, apellido, email, telefono } = await req.json(); const { nombre, apellido, email, telefono } = await req.json();
@@ -62,17 +63,29 @@ export async function PATCH(req: NextRequest) {
if (telefonoTomado) if (telefonoTomado)
return NextResponse.json({ error: "Ese teléfono ya está registrado" }, { status: 409 }); return NextResponse.json({ error: "Ese teléfono ya está registrado" }, { status: 409 });
const cliente = await prisma.cliente.update({ // Entre la comprobación de arriba y este update otra request puede tomar el mismo
where: { id: sesion.sub }, // email o teléfono; los índices únicos lo cortan y acá se traduce a un 409 legible.
data: { let cliente;
nombre: nombre.trim(), try {
apellido: apellido.trim(), cliente = await prisma.cliente.update({
email: emailNuevo, where: { id: sesion.sub },
telefono: aLocalArgentino(telefonoNorm), data: {
telefonoNorm, nombre: nombre.trim(),
}, apellido: apellido.trim(),
select: { nombre: true, apellido: true, email: true, telefonoNorm: true }, email: emailNuevo,
}); telefono: aLocalArgentino(telefonoNorm),
telefonoNorm,
},
select: { nombre: true, apellido: true, email: true, telefonoNorm: true },
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002")
return NextResponse.json(
{ error: "Ese email o teléfono ya está registrado" },
{ status: 409 }
);
throw e;
}
return NextResponse.json({ return NextResponse.json({
ok: true, ok: true,
+9
View File
@@ -31,6 +31,7 @@ export default function PerfilPage() {
fetch("/api/cliente/perfil") fetch("/api/cliente/perfil")
.then((r) => { .then((r) => {
if (r.status === 401) { router.push("/cliente"); return null; } if (r.status === 401) { router.push("/cliente"); return null; }
if (!r.ok) throw new Error("respuesta con error");
return r.json(); return r.json();
}) })
.then((data) => { .then((data) => {
@@ -38,6 +39,11 @@ export default function PerfilPage() {
setPerfil(data); setPerfil(data);
setForm(data); setForm(data);
setLoading(false); setLoading(false);
})
// Sin esto, si el pedido falla la pantalla se queda en "Cargando..." para siempre.
.catch(() => {
setError("No pudimos cargar tus datos. Probá recargar la página.");
setLoading(false);
}); });
}, [router]); }, [router]);
@@ -74,6 +80,9 @@ export default function PerfilPage() {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(form), body: JSON.stringify(form),
}); });
// Si la sesión venció mientras editaba, mandarlo a entrar de nuevo en vez de
// mostrarle un error suelto que no explica nada.
if (res.status === 401) { router.push("/cliente"); return; }
const data = await res.json(); const data = await res.json();
setGuardando(false); setGuardando(false);
if (!res.ok) { setError(data.error || "No se pudieron guardar los cambios"); return; } if (!res.ok) { setError(data.error || "No se pudieron guardar los cambios"); return; }
+8 -2
View File
@@ -28,14 +28,20 @@ export async function crearSesionJefe(jefeId: string): Promise<string> {
} }
export async function verificarSesion( export async function verificarSesion(
cookieName: string cookieName: string,
rolEsperado?: "admin" | "cliente" | "jefe"
): Promise<{ sub: string; role: string } | null> { ): Promise<{ sub: string; role: string } | null> {
const cookieStore = await cookies(); const cookieStore = await cookies();
const token = cookieStore.get(cookieName)?.value; const token = cookieStore.get(cookieName)?.value;
if (!token) return null; if (!token) return null;
try { try {
const { payload } = await jwtVerify(token, SECRET); const { payload } = await jwtVerify(token, SECRET);
return payload as { sub: string; role: string }; const sesion = payload as { sub: string; role: string };
// Los tres tipos de sesión se firman con el mismo secreto, así que un token de
// admin o de jefe puesto en la cookie del cliente pasa la verificación de firma.
// Cuando el rol importa, se comprueba acá.
if (rolEsperado && sesion.role !== rolEsperado) return null;
return sesion;
} catch { } catch {
return null; return null;
} }