Autenticação
Visão Geral
Autenticação centralizada no BC user/ — todos os apps web (adm/web, cms/web, hub/web) consomem o mesmo fluxo de login. Não há SSO multi-provider; um usuário, um cookie/sessão.
Hashing com argon2id (via @node-rs/argon2), sessões em Redis com TTL, identidade no cookie HttpOnly. Sem JWT.
Modelos
usr_users (banco public, schema usr)
| Campo | Tipo | Descrição |
|---|---|---|
id | text (nanoid 21) | PK |
email | text (unique) | Login |
name | text | Nome de exibição |
password | text | Hash argon2id |
type | text (enum) | staff (Hypergestor) ou tenant (usuário de um tenant) |
tenant_id | text (FK → tnt_tenants) | Nulo para staff; obrigatório para tenant |
created_at | timestamptz |
Staff vs tenant é semântico: ambos passam pelo mesmo login, mas o type controla acesso (staff acessa o adm/web; tenant acessa cms/web e hub/web).
Sessões (Redis)
Chave: session:<sessionId> (nanoid 21)
Valor: JSON { user_id, type, tenant_id, ttl_seconds?, impersonation? }
TTL padrão: 24h (renovado em cada request via sliding expiry). Sessões impersonadas usam ttl_seconds: 1800 e o plugin respeita esse override no sliding.
Endpoints (user/api, base_path /auth)
| Action | Método | Path | Body | Response |
|---|---|---|---|---|
login | POST | /login | { email, password } | { user } + cookie hg_session (HttpOnly) |
logout | POST | /logout | — | { ok: true } + cookie limpo |
me | GET | /me | — | { user, impersonation: { tenant_id, tenant_name, started_at } | null } |
impersonate | POST | /impersonate | { tenant_id } | { user, impersonation: { tenant_id, tenant_name, started_at } } + cookie novo (30 min) |
unimpersonate | POST | /unimpersonate | — | { user, impersonation: null } + cookie restaurado pra sessão pai (ou 401 se expirou) |
Todas declaradas em user/schemas/auth.yaml e consumidas via client tipado em @hg/shared/user/auth.
Impersonation (staff)
Staff pode "entrar como" um tenant para auditoria/suporte. Fluxo:
- Staff loga normalmente em
adm/web→ sessão pai em Redis (type: 'staff', TTL 24h). - Clicar "Entrar como" em
/tenants→POST /auth/impersonate { tenant_id }. - user-api gera nova sessão filha com
type: 'tenant',tenant_id: <alvo>,ttl_seconds: 1800,impersonation: { acting_staff_id, started_at, parent_session_id }. Cookie é sobrescrito com o token da filha. - TTL da sessão pai é reduzido pra 30min também — se staff não voltar, ambas expiram juntas.
- Registro em
usr.usr_impersonation_events(banco public):acting_staff_id,tenant_id,session_token_hash(sha256),started_at,ip,user_agent. /auth/mepassa a retornarimpersonation: { tenant_id, tenant_name, started_at }, ecms/web/hub/webmostram<hg-banner variant="warning">no topo com ações "Trocar pro CMS/Hub" e "Sair do impersonation".- Sair:
POST /auth/unimpersonate→ cookie volta pro token pai, TTL da pai volta a 24h,ended_até preenchido no audit.
Decisões de design:
- Durante impersonation
session.type === 'tenant'(não'staff') — autorização runtime bate com contexto, não precisa de branching nos handlers. Identidade real da staff fica emsession.impersonation.acting_staff_id(apenas auditoria). user_iddurante impersonation aponta prastaff.id. Handlers que gravam "created_by" usam essa identidade real. Se precisarem distinguir, leemreq.impersonation?.acting_staff_id ?? req.userId.- Cookie único
hg_session— abriradm/web+hub/webimpersonado em abas paralelas no mesmo navegador não é suportado (cookie é sobrescrito). Padrão GitHub/Stripe. - Audit nunca grava o token cru, só
sha256(token).
Integração com RLS
O cookie de sessão dá o user_id e, quando type === 'tenant', o tenant_id. O middleware do Fastify expõe ambos via req.tenantId, que é lido pelo withTenant() para setar app.tenant_id na connection Postgres — ativando RLS em todas as queries subsequentes.
Rotas staff (em adm/web → platform/tenant/user APIs) pulam o withTenant e usam publicDb diretamente (sem RLS). GET /tenants (tenant-api) filtra por req.userType: staff vê todos, tenant só o próprio.
Seeds
Usuários de desenvolvimento ficam em db/public/seed.ts. Senha padrão: hypergestor.