feat: el cliente puede ver y modificar sus datos de contacto #6
@@ -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 },
|
||||
|
ai-bot
commented
Guardás el teléfono en formato local en el campo #ai-review-inline Guardás el teléfono en formato local en el campo `telefono` y en formato WhatsApp en `telefonoNorm`. El GET lee `telefonoNorm` primero y hace fallback a `telefono`. Si `telefonoNorm` existe, `telefono` es redundante. Confirmá que el schema tenga `@@unique([telefonoNorm])` y `@@unique([email])` para que la BD sea la fuente de verdad.
#ai-review-inline
|
||||
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,
|
||||
|
||||
@@ -31,6 +31,7 @@ export default function PerfilPage() {
|
||||
fetch("/api/cliente/perfil")
|
||||
|
ai-bot
commented
El fetch inicial no maneja errores de red: si falla la request (p.ej. sin conexión), el #ai-review-inline El fetch inicial no maneja errores de red: si falla la request (p.ej. sin conexión), el `.then` no se ejecuta y `loading` queda en `true` para siempre. Agregá `.catch(() => { setLoading(false); setError('Error de conexión'); })` o usá try/catch con async/await.
```suggestion
useEffect(() => {
async function cargar() {
try {
const r = await fetch('/api/cliente/perfil');
if (r.status === 401) { router.push('/cliente'); return; }
const data = await r.json();
setPerfil(data);
setForm(data);
} catch {
setError('Error de conexión');
} finally {
setLoading(false);
}
}
cargar();
}, [router]);
```
#ai-review-inline
|
||||
.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; }
|
||||
|
||||
@@ -28,14 +28,20 @@ export async function crearSesionJefe(jefeId: string): Promise<string> {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
Importás
verificarSesiondesdeauth-admin.tspara validar la cookiecliente_session. Verificá que esa función valide correctamente el rol de cliente y no solo la existencia de la sesión de admin. Si reutiliza lógica de admin, podría haber escalada de privilegios.#ai-review-inline