Saltar al contenido principal

Auth de dos capas

El sistema de autenticación tiene dos cookies JWT con propósitos distintos:

Las dos cookies

CookieClaimsPropósitoExpira
identity_tokenIdentityClaims{IdentityID, Email}Acceso al hub de workspaces24h
admin_tokenAdminClaims{UserID, Email, Role, TenantID, Verified}Sesión dentro de un tenant24h

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ónComportamiento
activePermitido
trialing (no vencido)Permitido
trialing (vencido)Bloqueado → /admin/activate
Sin fila de suscripciónPermitido (legacy)
past_due, canceled, unpaidBloqueado → /admin/activate

Al entrar a una tienda (Enter)

El endpoint /admin/workspaces/:tenant_id/enter:

  1. Valida identity_token
  2. Verifica membresía server-side: SELECT ... FROM merchant_users WHERE identity_id=$1 AND tenant_id=$2
  3. Emite admin_token con el role y tenant de esa membresía
  4. Redirect a /admin

Nunca confía en el cliente para el tenant_id — siempre valida en la BD.