diff --git a/src/app/api/cliente/perfil/route.ts b/src/app/api/cliente/perfil/route.ts index 16d6887..1972006 100644 --- a/src/app/api/cliente/perfil/route.ts +++ b/src/app/api/cliente/perfil/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { Prisma } from "@prisma/client"; import { prisma } from "@/lib/prisma"; import { verificarSesion } from "@/lib/auth-admin"; 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 // (sin el +54 9) porque el formulario lo muestra como prefijo fijo. 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 }); const cliente = await prisma.cliente.findUnique({ @@ -26,7 +27,7 @@ export async function GET() { } 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 }); const { nombre, apellido, email, telefono } = await req.json(); @@ -62,17 +63,29 @@ export async function PATCH(req: NextRequest) { if (telefonoTomado) return NextResponse.json({ error: "Ese teléfono ya está registrado" }, { status: 409 }); - const cliente = await prisma.cliente.update({ - where: { id: sesion.sub }, - data: { - nombre: nombre.trim(), - apellido: apellido.trim(), - email: emailNuevo, - telefono: aLocalArgentino(telefonoNorm), - telefonoNorm, - }, - select: { nombre: true, apellido: true, email: true, telefonoNorm: true }, - }); + // Entre la comprobación de arriba y este update otra request puede tomar el mismo + // email o teléfono; los índices únicos lo cortan y acá se traduce a un 409 legible. + let cliente; + try { + cliente = await prisma.cliente.update({ + where: { id: sesion.sub }, + data: { + nombre: nombre.trim(), + apellido: apellido.trim(), + 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({ ok: true, diff --git a/src/app/cliente/perfil/page.tsx b/src/app/cliente/perfil/page.tsx index 1c12079..cf82f4c 100644 --- a/src/app/cliente/perfil/page.tsx +++ b/src/app/cliente/perfil/page.tsx @@ -31,6 +31,7 @@ export default function PerfilPage() { fetch("/api/cliente/perfil") .then((r) => { if (r.status === 401) { router.push("/cliente"); return null; } + if (!r.ok) throw new Error("respuesta con error"); return r.json(); }) .then((data) => { @@ -38,6 +39,11 @@ export default function PerfilPage() { setPerfil(data); setForm(data); 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]); @@ -74,6 +80,9 @@ export default function PerfilPage() { headers: { "Content-Type": "application/json" }, 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(); setGuardando(false); if (!res.ok) { setError(data.error || "No se pudieron guardar los cambios"); return; } diff --git a/src/lib/auth-admin.ts b/src/lib/auth-admin.ts index b632d71..7d508e3 100644 --- a/src/lib/auth-admin.ts +++ b/src/lib/auth-admin.ts @@ -28,14 +28,20 @@ export async function crearSesionJefe(jefeId: string): Promise { } export async function verificarSesion( - cookieName: string + cookieName: string, + rolEsperado?: "admin" | "cliente" | "jefe" ): Promise<{ sub: string; role: string } | null> { const cookieStore = await cookies(); const token = cookieStore.get(cookieName)?.value; if (!token) return null; try { 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 { return null; }