Pular para o conteúdo principal

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)

CampoTipoDescrição
idtext (nanoid 21)PK
emailtext (unique)Login
nametextNome de exibição
passwordtextHash argon2id
typetext (enum)staff (Hypergestor) ou tenant (usuário de um tenant)
tenant_idtext (FK → tnt_tenants)Nulo para staff; obrigatório para tenant
created_attimestamptz

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)

ActionMétodoPathBodyResponse
loginPOST/login{ email, password }{ user } + cookie hg_session (HttpOnly)
logoutPOST/logout{ ok: true } + cookie limpo
meGET/me{ user, impersonation: { tenant_id, tenant_name, started_at } | null }
impersonatePOST/impersonate{ tenant_id }{ user, impersonation: { tenant_id, tenant_name, started_at } } + cookie novo (30 min)
unimpersonatePOST/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:

  1. Staff loga normalmente em adm/web → sessão pai em Redis (type: 'staff', TTL 24h).
  2. Clicar "Entrar como" em /tenantsPOST /auth/impersonate { tenant_id }.
  3. 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.
  4. TTL da sessão pai é reduzido pra 30min também — se staff não voltar, ambas expiram juntas.
  5. Registro em usr.usr_impersonation_events (banco public): acting_staff_id, tenant_id, session_token_hash (sha256), started_at, ip, user_agent.
  6. /auth/me passa a retornar impersonation: { tenant_id, tenant_name, started_at }, e cms/web/hub/web mostram <hg-banner variant="warning"> no topo com ações "Trocar pro CMS/Hub" e "Sair do impersonation".
  7. 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 em session.impersonation.acting_staff_id (apenas auditoria).
  • user_id durante impersonation aponta pra staff.id. Handlers que gravam "created_by" usam essa identidade real. Se precisarem distinguir, leem req.impersonation?.acting_staff_id ?? req.userId.
  • Cookie único hg_session — abrir adm/web + hub/web impersonado 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.