Auth de dos capas
El sistema de autenticación tiene dos cookies JWT con propósitos distintos:
Las dos cookies
| Cookie | Claims | Propósito | Expira |
|---|---|---|---|
identity_token | IdentityClaims{IdentityID, Email} | Acceso al hub de workspaces | 24h |
admin_token | AdminClaims{UserID, Email, Role, TenantID, Verified} | Sesión dentro de un tenant | 24h |
Ambas usan el mismo secreto (ADMIN_JWT_SECRET) pero con estructuras de claims diferentes.
Flujo de login
1. POST /admin/login (email + password)
2. Autenticar contra merchant_identities WHERE email
3. Listar membresías:
SELECT mu.tenant_id, mu.role, t.name, t.slug, t.plan
FROM merchant_users mu JOIN tenants t ON t.id = mu.tenant_id
WHERE mu.identity_id = $1 AND t.status <> 'suspended'
4. Ramas:
├── 0 tiendas → /admin/workspaces (empty-state)
├── 1 tienda → admin_token directo → /admin (sin fricción)
└── N tiendas → identity_token → /admin/workspaces (selector)
Middlewares
RequireIdentity (nuevo)
Protege el hub de workspaces. Valida identity_token y extrae identity_id + email. No requiere tenant_id.
/admin/workspaces/* → RequireIdentity → WorkspaceHandler
AuthMiddleware (existente)
Protege el admin de un tenant. Valida admin_token y extrae UserID, Email, Role, TenantID.
/admin/* → AuthMiddleware → TenantInfo → Handler
RequirePlanActive (nuevo)
Bloquea tiendas con suscripción vencida. Redirige a /admin/activate.
/admin/* → AuthMiddleware → RequirePlanActive(db) → Handler
| Estado de suscripción | Comportamiento |
|---|---|
active | Permitido |
trialing (no vencido) | Permitido |
trialing (vencido) | Bloqueado → /admin/activate |
| Sin fila de suscripción | Permitido (legacy) |
past_due, canceled, unpaid | Bloqueado → /admin/activate |
Al entrar a una tienda (Enter)
El endpoint /admin/workspaces/:tenant_id/enter:
- Valida
identity_token - Verifica membresía server-side:
SELECT ... FROM merchant_users WHERE identity_id=$1 AND tenant_id=$2 - Emite
admin_tokencon el role y tenant de esa membresía - Redirect a
/admin
Nunca confía en el cliente para el tenant_id — siempre valida en la BD.