feat: el cliente puede ver y modificar sus datos de contacto #6
@@ -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,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default function PerfilPage() {
|
|||||||
fetch("/api/cliente/perfil")
|
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) => {
|
.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; }
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
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