Ir al contenido

Cómo habla el panel con el backend

El panel de gestión es una aplicación aparte. No comparte proceso con el backend, no comparte base de datos y no importa su código: le habla por HTTP, como se lo hablaría cualquier otro programa.

Esta página explica por qué está montado así, que es lo que no se deduce leyendo el código.

Es la decisión de la que cuelga todo lo demás.

Lo habitual en un panel de administración es guardar el token de sesión en localStorage y mandarlo como Authorization: Bearer …. Funciona, es cómodo, y tiene un problema que no se ve hasta que es tarde: cualquier ejecución de JavaScript ajeno en el panel se lo lleva. Una dependencia comprometida, un campo de texto que se pinta sin escapar, una extensión del navegador. Y con ese token se entra desde cualquier sitio, sin contraseña y sin dejar un solo intento fallido en el registro que alguien pueda mirar.

Aquí el token vive en el servidor del propio panel. El navegador solo tiene una cookie:

HttpOnly · Secure · SameSite=Strict · prefijo __Host-

HttpOnly significa que el JavaScript de la página no puede leerla. Un robo de token deja de ser posible porque no hay nada que robar en el navegador.

HttpOnly el script de la página no la ve
Secure no viaja fuera de HTTPS
SameSite=Strict no se manda en peticiones que vienen de otro sitio, que es lo que corta el CSRF
__Host- el navegador la ata a este dominio exacto y a /; un subdominio no puede sobrescribirla

⚠️ __Host- exige Secure, y el navegador rechaza en silencio una cookie __Host- que no lo lleve. No hay error, no hay aviso: el acceso parece correcto y la sesión no se guarda nunca — entrar, y volver al login. Por eso el prefijo solo se pone cuando la conexión lo admite (localhost, o HTTPS real mirando X-Forwarded-Proto). Si tu proxy no manda esa cabecera, no habrá prefijo, y conviene saberlo antes de perder una tarde.

Las pantallas no llaman al backend: llaman a /admin/api/backend/*, que es una ruta del panel. Ahí, en el servidor, se lee la cookie, se saca el token y se reenvía la petición.

Esto tiene tres consecuencias que valen más que la comodidad que quita:

  1. Deja de hacer falta CORS. El navegador habla con su propio origen. No hay ADMIN_CORS que ajustar ni orígenes que autorizar.
  2. El backend no queda expuesto al navegador. Solo el panel le habla.
  3. Las rutas están en una lista blanca del servidor. El proxy no reenvía a donde le digan: reenvía a lo que tiene permitido. Un parámetro manipulado no convierte el panel en un puente hacia otra máquina.

@pcreative/admin-client. Sin dependencias.

Antes había un SDK de terceros y se cambió tras mirar el uso real: de las llamadas del panel, la mitad larga eran una petición HTTP a pelo y el resto se repartía en una treintena de métodos. A cambio de ese envoltorio delgado, decenas de ficheros del panel quedaban atados a una dependencia externa y a su forma de nombrar las cosas.

Lo que sí hubo que replicar exactamente es el formato de la petición, porque eso no es una decisión de diseño: es lo que el backend espera al otro lado.

  • La consulta se serializa con corchetes: filtro[campo]=valor, y las listas indexadas, ids[0]=a&ids[1]=b.
  • Los nulos se omiten.
  • Las cookies viajan.
  • El error lleva el mensaje del backend, no el genérico del código de estado.

Equivocarse en cualquiera de esas cuatro no da ningún error: se manda un filtro que no casa con nada, el backend responde 200 con una lista vacía, y la pantalla dice «no hay productos» con el catálogo lleno. Por eso el cliente lleva pruebas sobre el formato y no sobre que «funcione».

El backend al que se habla se resuelve según el tema activo: al activar un tema y recargar, el panel pasa a hablar con el backend de ese tema.

La sesión es de cada backend, así que al cambiar de tienda hay que volver a entrar. No es un descuido: que una sesión sirviera para varias tiendas convertiría el panel en una llave maestra.

En Los paneles y sus dominios está la configuración de Apache con subdominios y sin ellos. Lo único que no se puede olvidar es X-Forwarded-Proto, por lo dicho arriba.