Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95ccf695c1 | |||
| 8fcd0fbf77 | |||
| 31bd0ad0bd | |||
| 2bdf76ab63 | |||
| f4df90ac1e |
@@ -33,6 +33,13 @@ services:
|
||||
GOOGLE_CLIENT_EMAIL: ${GOOGLE_CLIENT_EMAIL}
|
||||
GOOGLE_SHEET_ID: ${GOOGLE_SHEET_ID}
|
||||
GOOGLE_PRIVATE_KEY: ${GOOGLE_PRIVATE_KEY}
|
||||
# Bot de WhatsApp. Sin estas cuatro el .env de produccion no llega al
|
||||
# contenedor: los envios avisan que falta el token y el webhook contesta
|
||||
# 403 porque no puede validar la firma. Ver docs/whatsapp.md.
|
||||
WHATSAPP_PHONE_NUMBER_ID: ${WHATSAPP_PHONE_NUMBER_ID}
|
||||
WHATSAPP_TOKEN: ${WHATSAPP_TOKEN}
|
||||
WHATSAPP_VERIFY_TOKEN: ${WHATSAPP_VERIFY_TOKEN}
|
||||
WHATSAPP_APP_SECRET: ${WHATSAPP_APP_SECRET}
|
||||
ports:
|
||||
- "3004:3000"
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Alta de un cliente nuevo
|
||||
|
||||
Qué hay que pedirle a cada taller y qué hacemos nosotros, para poner en marcha el bot de
|
||||
reservas por WhatsApp. El detalle técnico de las piezas está en [whatsapp.md](whatsapp.md);
|
||||
acá está el orden operativo y quién hace cada cosa.
|
||||
|
||||
Cada cliente necesita **su propia** cuenta de Meta, su propia app y su propio número.
|
||||
Nada se comparte entre clientes: ni el token, ni el App Secret, ni el número.
|
||||
`Taller Beldor` es nuestro ambiente de pruebas y no sirve como cuenta productiva de nadie.
|
||||
|
||||
Tiempo realista: **una tarde** si el cliente tiene todo a mano, **varios días** si hay que
|
||||
esperar la aprobación del Display Name o liberar el número de WhatsApp.
|
||||
|
||||
---
|
||||
|
||||
## 1. Lo que le pedimos al cliente antes de empezar
|
||||
|
||||
Conviene mandarle esta lista completa de entrada, porque casi todo depende de él.
|
||||
|
||||
| Qué | Para qué | Cuidado |
|
||||
|---|---|---|
|
||||
| Quién va a ser **titular** de la cuenta de Meta | Todo cuelga de esa cuenta | Si es el dueño, mejor; si es un empleado y se va, se complica |
|
||||
| **Cuenta de Facebook** de esa persona, con sesión iniciada | Es la puerta de entrada a todo | — |
|
||||
| **Celular con SMS** para verificar esa cuenta | Meta exige verificar la cuenta de desarrollador | **El número tiene que estar confirmado en el Centro de cuentas ANTES de empezar** |
|
||||
| **Nombre público exacto** del taller | Nombre del portfolio comercial y base del Display Name | Sin caracteres especiales |
|
||||
| **Email del negocio** | Contacto del portfolio y de la app | Tiene que poder recibir el mail de verificación |
|
||||
| **Número de WhatsApp** que va a atender | Es el número del bot | **No puede estar activo en WhatsApp normal ni Business** (ver punto 5) |
|
||||
| **Jefes mecánicos** con nombre | El bot los ofrece como botones | Más de 3 → los primeros 3 son botón, el resto se elige por número |
|
||||
| **Tipos de servicio** y duración | El bot los ofrece después del mecánico | Pueden ser globales o propios de cada jefe |
|
||||
| **Horarios de atención** | Cálculo de los turnos disponibles | — |
|
||||
|
||||
## 2. Cuentas de Meta — lo hace el cliente, nosotros guiamos
|
||||
|
||||
Son actos que quedan a nombre del cliente: aceptan condiciones legales y crean cuentas.
|
||||
**Nosotros no los hacemos por él**, aunque estemos compartiendo pantalla.
|
||||
|
||||
1. **Confirmar el teléfono** en el Centro de cuentas → Perfiles y datos personales →
|
||||
Información de contacto → Agregar número → **Confirmar número** con el código del SMS.
|
||||
2. **Portfolio comercial** en `business.facebook.com`: nombre del negocio, su nombre, email.
|
||||
3. **Cuenta de desarrollador** en `developers.facebook.com` → "Empezar". Cuatro pasos:
|
||||
Register (acepta condiciones) → Verify account (el SMS) → Contact info → About you.
|
||||
4. **Crear la app**: tipo Business, caso de uso **"Conectarte con los clientes a través de
|
||||
WhatsApp"**, vinculada al portfolio del paso 2.
|
||||
|
||||
> **El SMS es el punto donde más veces se traba el alta.** Si no llega: verificar que el
|
||||
> número ya esté confirmado en el Centro de cuentas (es un SMS distinto del de la cuenta de
|
||||
> desarrollador), revisar la bandeja de spam del teléfono, y **no reintentar muchas veces
|
||||
> seguidas** — Meta deja de entregar los códigos por un rato sin avisar. Si pasó eso,
|
||||
> esperar unas horas y pedir uno solo.
|
||||
|
||||
## 3. Credenciales — las saca el cliente, nosotros no las tocamos
|
||||
|
||||
Las cuatro variables del `.env`. **Nunca pasan por chat, mail ni WhatsApp**: el cliente las
|
||||
copia de la pantalla de Meta y las pega directo en el `.env` del servidor, o nos da acceso
|
||||
al servidor y las carga él ahí.
|
||||
|
||||
| Variable | De dónde sale | Forma esperada |
|
||||
|---|---|---|
|
||||
| `WHATSAPP_PHONE_NUMBER_ID` | Caso de uso WhatsApp → Paso 1 "Pruébalo" | Numérico, ~16 dígitos. **No es el teléfono** |
|
||||
| `WHATSAPP_TOKEN` | Business Settings → Usuarios del sistema → Generar token | **Empieza con `EAA`**, ~200 chars |
|
||||
| `WHATSAPP_VERIFY_TOKEN` | Lo inventamos nosotros | Alfanumérico, sin símbolos ni espacios |
|
||||
| `WHATSAPP_APP_SECRET` | App → Configuración → Básica → "Mostrar" | **32 caracteres hexadecimales** |
|
||||
|
||||
**System user** (de donde sale el token):
|
||||
|
||||
- Crear uno por cliente, por ejemplo `taller-bot`, rol Employee.
|
||||
- Asignarle **dos activos**: la app (permiso *Desarrollar app*) y la cuenta de WhatsApp
|
||||
(permisos *Mensajes* y *Números de teléfono — ver y administrar*).
|
||||
- Generar token con caducidad **Nunca** y los permisos `whatsapp_business_messaging` y
|
||||
`whatsapp_business_management`.
|
||||
- **El token se muestra una sola vez.** Tener el `.env` abierto antes de apretar el botón.
|
||||
|
||||
Errores que ya nos pasaron y conviene chequear:
|
||||
|
||||
- Token **cortado al copiar**: si no empieza con `EAA`, está incompleto.
|
||||
- Placeholders sin reemplazar: que no queden `<...>` en el `.env`.
|
||||
- `APP_SECRET` que no tiene 32 hex: quedó truncado.
|
||||
- Sin `APP_SECRET` correcto, **el webhook rechaza todo con 403** y no entra ningún mensaje.
|
||||
|
||||
## 4. Lo que hacemos nosotros
|
||||
|
||||
1. Desplegar la app para ese cliente con su dominio propio y certificado válido.
|
||||
Meta **exige HTTPS** para el webhook.
|
||||
2. Cargar las 4 variables en el `.env` de ese despliegue y `docker compose up -d`.
|
||||
El `.env` no está en el repo: un `git push` no lo toca.
|
||||
3. Verificar el webhook **antes** de darlo de alta en Meta:
|
||||
|
||||
```bash
|
||||
curl "https://<dominio-del-cliente>/api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=<VERIFY_TOKEN>&hub.challenge=12345"
|
||||
```
|
||||
|
||||
Tiene que devolver exactamente `12345` en texto plano. Si devuelve 403, la variable no
|
||||
llegó al contenedor.
|
||||
4. Verificar que el token funciona contra la Graph API:
|
||||
|
||||
```bash
|
||||
curl "https://graph.facebook.com/v21.0/<PHONE_NUMBER_ID>?fields=display_phone_number,verified_name" \
|
||||
-H "Authorization: Bearer <TOKEN>"
|
||||
```
|
||||
5. Dar de alta el webhook en Meta: **WhatsApp → Configuration → Webhook**, con la Callback
|
||||
URL del cliente, el mismo verify token, y **suscribir el campo `messages`**.
|
||||
Sin esa suscripción no llega ningún mensaje.
|
||||
6. Cargar los datos del taller: jefes mecánicos, tipos de servicio y horarios.
|
||||
7. Probar punta a punta con el número de prueba gratis de Meta, mirando
|
||||
`docker logs -f taller_app`.
|
||||
|
||||
## 5. Migrar el número real del taller
|
||||
|
||||
El último paso, y el que más fricción genera con el cliente.
|
||||
|
||||
- El número **no puede estar activo en la app de WhatsApp normal o Business**. Hay que
|
||||
**borrar esa cuenta** antes de migrarlo a Cloud API. Se pierde el historial de chats.
|
||||
- Avisarle al cliente con tiempo: si el taller venía atendiendo por ese número, va a querer
|
||||
elegir el momento.
|
||||
- El **Display Name** necesita aprobación de Meta. Suele salir rápido, pero puede demorar.
|
||||
- La **Business Verification** se saltea mientras el cliente esté abajo de 1000 usuarios
|
||||
únicos cada 24hs. Para un taller eso es muchísimo, así que en la práctica no aplica.
|
||||
|
||||
## 6. Costos a aclarar antes de vender
|
||||
|
||||
- El flujo del bot ocurre **dentro de la ventana de 24hs que abre el cliente final**, así que
|
||||
**no usa plantillas aprobadas y no tiene costo por conversación**.
|
||||
- Si en algún momento se quisiera que el taller **inicie** la conversación (recordatorios de
|
||||
turno, por ejemplo), eso **sí** requiere plantillas aprobadas y **sí** tiene costo.
|
||||
Hoy el bot no lo hace.
|
||||
+3
-1
@@ -35,7 +35,9 @@ puede confiar en el payload).
|
||||
Para el token definitivo: Business Settings → System Users → crear usuario → Generate Token
|
||||
con permisos `whatsapp_business_messaging` y `whatsapp_business_management`.
|
||||
4. **App → Settings → Basic → App Secret** → `WHATSAPP_APP_SECRET`.
|
||||
5. Cargar las 4 variables en el `.env` de producción y redeployar.
|
||||
5. Cargar las 4 variables en el `.env` de producción (`/home/proyecto/taller_automotriz/.env`,
|
||||
al lado del `docker-compose.yml`, que es de donde compose las interpola) y levantar de nuevo
|
||||
con `docker compose up -d`. Un `git push` no alcanza: el `.env` no está en el repo.
|
||||
6. **WhatsApp → Configuration → Webhook**:
|
||||
- Callback URL: `https://taller.beldo.duckdns.org/api/whatsapp/webhook`
|
||||
- Verify token: el mismo `WHATSAPP_VERIFY_TOKEN`
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
@@ -20,7 +20,7 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { autoId, tipoServicioId, descripcion, observaciones, fechaRealizado } = await req.json();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { verificarSesion } from "@/lib/auth-admin";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { verificarSesion } from "@/lib/auth-admin";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function GET() {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const jefes = await prisma.jefeMecanico.findMany({
|
||||
@@ -15,7 +15,7 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { nombre, email, password, slotsParalelos } = await req.json();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
export async function GET() {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const servicios = await prisma.tipoServicio.findMany({ orderBy: { nombre: "asc" } });
|
||||
@@ -11,7 +11,7 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { nombre, duracionHoras } = await req.json();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { verificarSesion } from "@/lib/auth-admin";
|
||||
import { agregarFilaSheet } from "@/lib/google-sheets";
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sesion = await verificarSesion("admin_session");
|
||||
const sesion = await verificarSesion("admin_session", "admin");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
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 autos = await prisma.auto.findMany({
|
||||
@@ -15,7 +15,7 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(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 { patente, marca, modelo, anio, nroChasis } = await req.json();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
export async function GET(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 { searchParams } = new URL(req.url);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
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 jefes = await prisma.jefeMecanico.findMany({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { whereTrabajosDeJefe } from "@/lib/servicios";
|
||||
// Trabajos que ofrece un jefe mecánico: los suyos + los globales del taller.
|
||||
// El flujo del cliente es jefe → trabajo, así que el jefe es obligatorio.
|
||||
export async function GET(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 jefeMecanicoId = req.nextUrl.searchParams.get("jefeMecanicoId");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { obtenerSlotsDisponibles } from "@/lib/scheduling";
|
||||
import { trabajoEsDeJefe } from "@/lib/servicios";
|
||||
|
||||
export async function GET(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 { searchParams } = new URL(req.url);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { verificarSesion } from "@/lib/auth-admin";
|
||||
import { crearTurno } from "@/lib/turnos";
|
||||
|
||||
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 turnos = await prisma.turno.findMany({
|
||||
@@ -17,7 +17,7 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(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 { autoId, tipoServicioId, jefeMecanicoId, fechaInicio, kilometros } = await req.json();
|
||||
|
||||
@@ -4,8 +4,8 @@ import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
// Edita un trabajo propio del jefe (nombre, duración, activo). Solo si le pertenece.
|
||||
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
const sesion = await verificarSesion("jefe_session", "jefe");
|
||||
if (!sesion)
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
@@ -42,8 +42,8 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
|
||||
// Elimina un trabajo propio. Si ya tiene turnos asociados, lo desactiva en vez de borrarlo
|
||||
// (para no romper la integridad referencial de los turnos históricos).
|
||||
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
const sesion = await verificarSesion("jefe_session", "jefe");
|
||||
if (!sesion)
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
@@ -4,8 +4,8 @@ import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
// Lista los trabajos cargados por el jefe logueado (los suyos, no los globales).
|
||||
export async function GET() {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
const sesion = await verificarSesion("jefe_session", "jefe");
|
||||
if (!sesion)
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const servicios = await prisma.tipoServicio.findMany({
|
||||
@@ -18,8 +18,8 @@ export async function GET() {
|
||||
|
||||
// Crea un trabajo propio del jefe (nombre + duración en horas).
|
||||
export async function POST(req: NextRequest) {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
const sesion = await verificarSesion("jefe_session", "jefe");
|
||||
if (!sesion)
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { nombre, duracionHoras } = await req.json();
|
||||
|
||||
@@ -3,8 +3,8 @@ import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
const sesion = await verificarSesion("jefe_session", "jefe");
|
||||
if (!sesion)
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
|
||||
+8
-6
@@ -3,12 +3,14 @@ import { jwtVerify } from "jose";
|
||||
|
||||
const SECRET = new TextEncoder().encode(process.env.NEXTAUTH_SECRET || "dev-secret");
|
||||
|
||||
async function verifyCookie(req: NextRequest, cookieName: string) {
|
||||
// Las tres sesiones se firman con el mismo secreto, así que además de la firma hay
|
||||
// que mirar el rol: si no, un token de jefe en la cookie del cliente pasa.
|
||||
async function verifyCookie(req: NextRequest, cookieName: string, rol: "admin" | "cliente" | "jefe") {
|
||||
const token = req.cookies.get(cookieName)?.value;
|
||||
if (!token) return false;
|
||||
try {
|
||||
await jwtVerify(token, SECRET);
|
||||
return true;
|
||||
const { payload } = await jwtVerify(token, SECRET);
|
||||
return (payload as { role?: string }).role === rol;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -21,7 +23,7 @@ export async function middleware(req: NextRequest) {
|
||||
pathname.startsWith("/cliente/historial") ||
|
||||
pathname.startsWith("/cliente/perfil") ||
|
||||
pathname.startsWith("/cliente/turnos")) {
|
||||
const ok = await verifyCookie(req, "cliente_session");
|
||||
const ok = await verifyCookie(req, "cliente_session", "cliente");
|
||||
if (!ok) return NextResponse.redirect(new URL("/cliente", req.url));
|
||||
}
|
||||
|
||||
@@ -31,12 +33,12 @@ export async function middleware(req: NextRequest) {
|
||||
pathname.startsWith("/admin/servicios") ||
|
||||
pathname.startsWith("/admin/jefes") ||
|
||||
pathname.startsWith("/admin/clientes")) {
|
||||
const ok = await verifyCookie(req, "admin_session");
|
||||
const ok = await verifyCookie(req, "admin_session", "admin");
|
||||
if (!ok) return NextResponse.redirect(new URL("/admin", req.url));
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/jefe/dashboard")) {
|
||||
const ok = await verifyCookie(req, "jefe_session");
|
||||
const ok = await verifyCookie(req, "jefe_session", "jefe");
|
||||
if (!ok) return NextResponse.redirect(new URL("/jefe", req.url));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user