Auth
Autenticación better-auth con los defaults FESA — schema drizzle, factory de servidor y cliente react, sin registro público.
@fesa/components/auth empaqueta el patrón de autenticación de la casa: better-auth + drizzle sobre MySQL, con administración de cuentas por rol y sin registro público por defecto. El primer consumidor es developers.fesa.com.pa.
Peers opcionales
Este módulo requiere better-auth (≥1.4) y drizzle-orm (≥0.40) instalados en la app. Son peers
opcionales: las apps que no usan auth no los necesitan.
Las tres piezas
| Export | Qué es |
|---|---|
createAuthTables({ roles, defaultRole }) | Las cuatro tablas drizzle que better-auth necesita (users, sessions, accounts, verifications) con el enum de roles parametrizado |
createFesaAuth(options) | La instancia better-auth con los defaults FESA |
createFesaAuthClient(options) — desde ./auth/client | El cliente react con el plugin de administración |
Defaults que vienen de fábrica
- Registro público deshabilitado (
allowSignUp: false): las cuentas las crea un administrador con el pluginadmin. - Contraseña mínima de 8, sesión de 7 días con cookie cache de 5 minutos.
- Rate limit por ruta: 5 intentos de login por 15 minutos, 3 resets por hora.
nextCookies()al final de los plugins — las Server Actions manejan cookies sin cableado extra.
Uso
import { createAuthTables } from '@fesa/components/auth'
export const ROLES = ['admin', 'programador', 'usuario'] as const
export const { users, sessions, accounts, verifications } = createAuthTables({
roles: ROLES,
defaultRole: 'usuario',
})import { createFesaAuth } from '@fesa/components/auth'
import { accounts, ROLES, sessions, users, verifications } from '@/db/schema'
import { db } from '@/lib/db'
export const auth = createFesaAuth({
appName: 'Mi App',
baseURL: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
secret: process.env.BETTER_AUTH_SECRET ?? '',
db,
tables: { users, sessions, accounts, verifications },
roles: ROLES,
defaultRole: 'usuario',
adminRoles: ['admin'],
})import { toNextJsHandler } from 'better-auth/next-js'
import { auth } from '@/lib/auth'
export const { GET, POST } = toNextJsHandler(auth.handler)'use client'
import { createFesaAuthClient } from '@fesa/components/auth/client'
export const authClient = createFesaAuthClient()Seed de la primera cuenta
Con el registro cerrado, la primera cuenta se siembra directo con drizzle. La constante
CREDENTIAL_ISSUER importa: desde better-auth 1.5 el login solo matchea cuentas credenciales
con issuer = 'local:credential' — sin ella el login responde 401 aunque el hash sea correcto.
import { randomUUID } from 'node:crypto'
import { hashPassword } from 'better-auth/crypto'
import { CREDENTIAL_ISSUER } from '@fesa/components/auth'
const userId = randomUUID()
await db.insert(users).values({ id: userId, name: 'Admin', email: '[email protected]', emailVerified: true, role: 'admin' })
await db.insert(accounts).values({
id: randomUUID(),
userId,
providerId: 'credential',
issuer: CREDENTIAL_ISSUER,
accountId: userId,
password: await hashPassword(process.env.SEED_ADMIN_PASSWORD!),
})Administración de usuarios
El plugin admin viene activo: el cliente expone authClient.admin.createUser, setRole,
banUser / unbanUser, setUserPassword, removeUser y listUsers (en servidor,
auth.api.listUsers con los headers de la petición). El patrón FESA: página server que lista y
componente cliente que muta y hace router.refresh().
Roles y tipos del cliente
El adminClient genérico tipa role como 'user' | 'admin'. Con roles propios, castea en el
punto de llamada — el servidor valida contra el enum real de la tabla.
Qué NO trae (a propósito)
- Ni páginas de login ni UI: cada app arma su formulario con su marca (el de developers son ~90 líneas).
- Ni verificación de email ni 2FA por defecto: para herramientas internas con cuentas creadas por un admin es fricción sin beneficio. Se agregan por
pluginssi un caso lo pide. - Ni migraciones: cada app corre
drizzle-kit pusho genera las suyas contra su base.