Autenticación
Kevin CMS autentica con better-auth sobre tu base de datos D1. En esta versión hay email y contraseña, con la dirección verificada por correo y restablecimiento de contraseña. Los proveedores sociales y el panel de login llegan después.
Prepáralo
Sección titulada «Prepáralo»-
nodejs_compaten tuwrangler.jsonc. better-auth usanode:cryptoy sin esa marca el sitio no arranca:wrangler.jsonc { "compatibility_flags": ["nodejs_compat"] } -
Un secreto con el que firmar las sesiones. En local va en
.dev.vars, que está en tu.gitignore:Ventana de terminal openssl rand -base64 32.dev.vars BETTER_AUTH_SECRET=el-valor-que-acabas-de-generar -
En producción, como secreto del Worker:
Ventana de terminal npx wrangler secret put BETTER_AUTH_SECRET -
Genera y aplica la migración con las tablas de sesión:
Ventana de terminal npx cms db:migratenpx cms db:apply
No hay ninguna opción que configurar en cms.config.ts: si el secreto está, la autenticación funciona.
La URL base
Sección titulada «La URL base»Por defecto, better-auth deduce el origen de cada petición, que es lo que quiere un Worker: funciona sin configurar nada y sigue funcionando en cualquier dominio de vista previa.
Si tu sitio vive detrás de un proxy y el Host que llega no es el público, pon la URL real en el
siteUrl de los ajustes y pasará a ser la URL base. De ella salen los enlaces de
verificación y de restablecimiento.
Las tablas
Sección titulada «Las tablas»El codegen emite cuatro tablas junto a las de tus colecciones, en el mismo .cms/schema.ts y en la misma
migración, para que no haya dos sistemas de migraciones conviviendo.
| Tabla | Qué guarda | ¿Es una colección? |
|---|---|---|
users |
Las cuentas | Sí |
session |
Las sesiones abiertas | No |
account |
Las credenciales, incluido el hash de la contraseña | No |
verification |
Los tokens de un solo uso | No |
Las tres que no son colecciones son fontanería: no aparecen en locals.cms.collections, ni en el admin, ni
en la API REST. Un GET /api/cms/session responde 404, igual que cualquier slug que no existe.
La colección users
Sección titulada «La colección users»La colección users del CMS es la tabla de usuarios de better-auth. Es un solo sitio: se gestionan
desde el admin como cualquier otra colección y a la vez son el sujeto de la sesión.
Si no la declaras, se añade sola. Si la declaras, se fusiona: tus campos se conservan y se añaden los que la autenticación necesita.
defineCollection({ slug: 'users', fields: [ { name: 'bio', type: 'textarea' }, { name: 'twitter', type: 'text' }, ],})Esa colección acaba con tus dos campos y con email, name, emailVerified e image. El email
acaba siempre required y unique aunque no lo declares: sin el índice único, dos cuentas podrían
compartir dirección.
Declarar uno de esos campos con un tipo incompatible —un email de tipo number— es un error de config
que nombra el campo y el tipo que se esperaba.
Los endpoints
Sección titulada «Los endpoints»/api/auth/* lo sirve better-auth entero. Kevin CMS no reimplementa nada.
| Método | Ruta | Qué hace |
|---|---|---|
POST |
/api/auth/sign-up/email |
Crea la primera cuenta, ya verificada. Después, 403 |
POST |
/api/auth/sign-in/email |
Inicia sesión, o 403 si falta verificar |
POST |
/api/auth/sign-out |
Cierra la sesión |
GET |
/api/auth/get-session |
La sesión actual, o null |
GET |
/api/auth/verify-email |
El enlace del correo de verificación |
POST |
/api/auth/send-verification-email |
Reenvía el correo de verificación |
POST |
/api/auth/request-password-reset |
Manda el correo de restablecimiento |
POST |
/api/auth/reset-password |
Guarda la contraseña nueva |
curl -X POST http://localhost:4321/api/auth/sign-up/email \ -H 'content-type: application/json' \ -d '{"email":"kevin@example.com","password":"una-contraseña-larga","name":"Kevin"}'La sesión viaja en una cookie, así que desde el navegador no tienes que hacer nada. Con curl, guarda las
cookies con -c y reenvíalas con -b.
Una contraseña incorrecta y un email que no existe dan la misma respuesta, un 401. Es deliberado:
distinguirlas permitiría averiguar qué direcciones tienen cuenta. Una cuenta sin verificar es distinta —
un 403— porque ahí sí hay algo que el usuario puede hacer.
Alta, verificación y sesión
Sección titulada «Alta, verificación y sesión»El primer usuario
Sección titulada «El primer usuario»La primera cuenta de una instalación nace verificada. No podría ser de otra forma: la verificación es obligatoria, y el correo que la desbloquea sale con el remitente que esa misma persona todavía no ha configurado. Así que se da de alta y entra, sin recibir nada.
Lo normal es no hacerlo a mano: la primera visita a /admin en una instalación vacía es el asistente de
configuración, que pide los ajustes del sitio y la primera cuenta en un solo formulario y deja la sesión
abierta.
Los demás
Sección titulada «Los demás»Todo usuario posterior —una vez el admin sepa invitarlos— sigue el flujo completo. El alta no deja sesión iniciada y no manda correo: el correo sale en el primer intento de iniciar sesión, que además se rechaza:
-
La cuenta se crea con la dirección sin verificar. La respuesta no trae cookie.
-
POST /api/auth/sign-in/emailresponde403, y en ese momento envía el correo de verificación.En local no hace falta buzón:
wrangler devsimula el envío y escribe la ruta del fichero con el mensaje en la consola. -
El enlace del correo apunta a
GET /api/auth/verify-email. Seguirlo marca la dirección. -
POST /api/auth/sign-in/emailresponde ahora200con la cookie.
¿Por qué en el inicio de sesión y no en el alta? Porque quien se da de alta y no vuelve nunca genera un
correo que nadie iba a abrir, y quien sí vuelve lo recibe justo cuando lo necesita — recién pedido, no
caducado. Si hace falta otro, POST /api/auth/send-verification-email lo reenvía.
Restablecer la contraseña
Sección titulada «Restablecer la contraseña»POST /api/auth/request-password-reset manda el correo. Su enlace lleva a
GET /api/auth/reset-password/:token, y la contraseña nueva se guarda con un POST a
/api/auth/reset-password. El token dura una hora.
Astro.locals.cms.auth
Sección titulada «Astro.locals.cms.auth»interface AuthLocals { getSession(): Promise<SessionData> // la sesión, o null getUser(): Promise<SessionUser | null> // el usuario, o null requireUser(): Promise<SessionUser> // el usuario, o lanza 401}Proteger una página son dos líneas:
---export const prerender = false
const user = await Astro.locals.cms.auth.getUser()if (!user) return Astro.redirect('/login')---
<p>Hola, {user.name}</p>getSession() se consulta una vez por petición: llamarlo en el layout y otra vez en un componente no
cuesta el doble, porque lo que se guarda es la promesa y no el valor —dos llamadas simultáneas también
comparten la consulta—. La caché muere con la petición, así que ninguna sesión se filtra a la siguiente.
Nada de esto se construye hasta que lo pides: una página que no lee cms no toca la base de datos.
requireUser() es para endpoints, donde un redirect no sirve: lanza un error que la API REST traduce a
un 401.
Qué protege la API REST
Sección titulada «Qué protege la API REST»Con la autenticación en su sitio, las reglas de la API REST por fin se pueden cumplir:
| Operación | Regla |
|---|---|
GET |
Pública si la colección declara access: { read: 'public' }; si no, pide sesión |
POST, PATCH, DELETE |
Piden sesión siempre |
Lo que todavía no hay
Sección titulada «Lo que todavía no hay»| Estado | |
|---|---|
| Pantalla para reenviar el correo de verificación | No. El endpoint existe, la pantalla llega con el admin |
| Formulario de restablecimiento | No. El flujo funciona por API; la pantalla llega con el admin |
| Invitar usuarios por correo | No. Llega con el alta de usuarios del admin |
| Google y GitHub | No. Se configurarán desde el admin, sin desplegar |
| Pantalla de login | Sí, en /admin/login |
| Cierre del registro tras el primer usuario | Sí. La primera cuenta lo cierra |
| Roles y permisos | No |