Pular para o conteúdo principal

Fluxo de Autenticação

Visão geral

O auth usa ApiClient no server e no client, com sessão baseada em cookie jwt_token HttpOnly.

┌──────────────────────────────────────────────┐
│ Server-side (loader) │
│ api.server.ts → ApiClient com/sem token │
│ Usado para: brand loading, auth profile │
├──────────────────────────────────────────────┤
│ Client-side (browser) │
│ auth.client.ts → ApiClient sem ler cookie │
│ Usado para: ações de UI pós-login │
└──────────────────────────────────────────────┘

Fluxo de inicialização

DefaultLayout monta
├── <AuthInitializer />
│ ├── useAuthInit():
│ │ └── Recebe auth do loader SSR → sincroniza user/userInfo no Zustand + marca hydrated
│ │ (NÃO chama initAuthService — apenas sync de dados)
│ ├── useAuthProfileSync():
│ │ └── Mantém profile atualizado quando necessário
│ └── useValidationRuntimeSync():
│ └── Computa allValidations e sincroniza com store

├── <WalletInitializer />
│ └── Roda em paralelo com AuthInitializer
│ └── Inicializa wallet state (saldo, transações)

O AuthInitializer chama 3 hooks: useAuthInit, useAuthProfileSync e useValidationRuntimeSync. O WalletInitializer roda ao lado do AuthInitializer, não dentro dele.

Login

Login e registro passam por API routes no server, não por chamadas diretas ao authService:

LoginModal
├── Usuário preenche email/CPF + senha
├── POST /api/auth/login (API route no server)
│ └── Server: authService.login() + Set-Cookie HttpOnly
├── API retorna: { user, userInfo, type? }
│ ├── type === 'two_factor_code' → modal 2FA (TODO)
│ ├── type === 'cancelled_account' → aviso conta cancelada
│ └── sem type → sucesso
├── setUser(user, userInfo) no Zustand
└── Fecha modal

Registro dinâmico (single-step)

RegisterModal
├── Usuário preenche formulário dinâmico (email, senha, telefone, documento, etc. conforme authConfig)
├── POST /api/auth/register (API route no server)
│ └── Server: valida payload + resolve endpoint em runtime
│ ├── /bff/register-simplified
│ ├── /auth/register
│ ├── /auth/register/simplified
│ └── /bff/social/{provider}/registerSimplified
│ + Set-Cookie HttpOnly
├── API retorna: { user, userInfo } (mesmo formato do login)
├── setUser(user, userInfo) no Zustand
└── Fecha modal

O fluxo social inicia em /api/auth/social/:provider (route interna do template), que redireciona para o provider BFF e retorna ao modal com os parâmetros do callback.

Nota: tanto login quanto registro passam pelas API routes (/api/auth/login, /api/auth/register) para que o cookie HttpOnly seja setado no server-side. O client nunca manipula o token diretamente.

Token storage

  • Cookie: jwt_token, 30 dias, path=/, SameSite=Lax, HttpOnly, Secure
  • Leitura do token: somente no server (getTokenFromRequest)
  • Zustand: mantém user, userInfo, isAuthenticated, authHydrated, authModal

Reatividade de UI (login/logout em tempo real)

Regra obrigatória: componentes que condicionam visibilidade/comportamento a "estar logado" devem ler o estado do Zustand store, não do route loader data.

Por quê

O _layout.tsx carrega o perfil no loader e expõe via useRouteLoaderData("routes/_layout").auth. Esse valor é um snapshot do SSR — não muda enquanto a rota não for revalidada. Como login/logout via modal só mutam a Zustand store (setAuthUser / clearAuth) sem navegar nem chamar revalidator.revalidate(), qualquer componente que leia do loader para esse fim fica preso no estado antigo até a próxima navegação completa.

Padrão errado

// ❌ Não faça isso em componente client
const layoutData = useRouteLoaderData("routes/_layout");
const isLoggedIn = Boolean(layoutData?.auth?.user?.id);
if (!isLoggedIn) return null; // fica travado até navegar

Padrão correto

// ✅ Store reativo, com fallback SSR-safe
import { useAccountsStore } from "@cactus-agents/accounts/react";

const storeAuthenticated = useAccountsStore((s) => s.isAuthenticated);
const authHydrated = useAccountsStore((s) => s.authHydrated);
const ssrAuthenticated = Boolean(layoutData?.auth?.user?.id); // só pra SSR paint
const isLoggedIn = authHydrated ? storeAuthenticated : ssrAuthenticated;

A authHydrated vira true logo após o useAuthInit rodar (primeiro commit do <AuthInitializer />). Antes disso, o store tem defaults (isAuthenticated = false), então o fallback do loader evita que o SSR markup discorde do primeiro client render.

Se o componente pode tolerar um flash "deslogado" na primeira renderização (ex: feature só interessante pra logado), pode ler direto do store sem fallback.

Casos equivalentes

A mesma regra vale pra qualquer valor que muda em runtime sem revalidar o loader:

O queFonte reativa correta
isAuthenticated, user, userInfouseAccountsStore
Wallet, transaçõesuseAccountsStore (wallet slice) ou store de wallet dedicada
Gamification (missões, torneios, níveis)useGamificationStore
Favoritos (slugs)useGamesStore.favoriteSlugs

Checklist ao criar novo componente auth-aware

  • Leitura de isAuthenticated vem de useAccountsStore, não de useRouteLoaderData?
  • Há fallback pra SSR via authHydrated (se zero-flash importa)?
  • Não há cache adicional de user.id em ref/state que ignora mudanças?
  • Se o componente dispara side-effects (ex: carregar dados do usuário), recomputa quando user?.id muda?

Logout

Header → botão "Sair"
├── POST /api/auth/logout (API route no server)
│ └── Server: authService.logout() + Set-Cookie de remoção (HttpOnly)
├── clearAuth() no Zustand
│ └── Também chama useWalletStore.getState().clearWallet() (side effect cross-store)

clearAuth — Side effect cross-store

O clearAuth() da auth store também limpa a wallet store:

// auth.ts
clearAuth: () => {
set({ user: null, userInfo: null, isAuthenticated: false });
useWalletStore.getState().clearWallet();
}

Isso garante que dados financeiros do usuário anterior não permaneçam no estado após logout ou expiração de sessão.

Tratamento de 401

Qualquer request protegido no server que falhar em autenticação resulta em estado não autenticado no loader/action. No client, a UI reflete esse estado via clearAuth() e novo ciclo de loader.

Validações pós-login

Após o login (ou page refresh com cookie), o sistema de validações avalia automaticamente se o usuário tem pendências obrigatórias:

setUser(user, userInfo)


useValidationRuntimeSync
├── buildValidationSnapshot(user, userInfo)
└── fetchAllValidations(config, snapshot)


allValidations.hasPending?
├── force/regulatory/global → ValidationBlockerOverlay (overlay bloqueante)
└── false → navegação normal

Três paths de bloqueio (em ordem de prioridade):

  1. forceforceRequestKyc = true no perfil (backend forçou KYC)
  2. regulatoryshowPendingDataFlow = true no perfil (dados regulatórios pendentes, ex: endereço)
  3. globalconfig.global.active = true e módulos do global não satisfeitos

Quando há pendência, o DefaultLayout renderiza o ValidationBlockerOverlay sobre o site (com blur e anti-tamper). O usuário precisa completar as validações para continuar.

Além do bloqueio global, cada contexto (casino, deposit, withdraw, etc.) é avaliado separadamente via ValidationStepsModal quando o usuário tenta executar a ação correspondente.

Ver Validações para detalhes completos.

Arquivos relevantes

ArquivoPapel
app/store/auth.tsZustand store: user, userInfo, isAuthenticated, hydrated, authModal + clearAuth (limpa wallet)
app/store/wallet.tsZustand store: wallet state + clearWallet (chamado pelo clearAuth)
app/services/auth.client.tsSingleton AuthService + ApiClient client-side
app/utils/cookie.server.tsgetTokenFromRequest, makeSetTokenHeader, makeDeleteTokenHeader
app/hooks/useAuthInit.tsSync do auth do loader SSR para store (NÃO chama initAuthService)
app/hooks/useAuthProfileSync.tsSync de profile atualizado
app/hooks/useValidationRuntimeSync.tsComputa allValidations e sincroniza com store
app/components/auth/AuthInitializer.tsxComponente invisível que chama useAuthInit + useAuthProfileSync + useValidationRuntimeSync
app/components/wallet/WalletInitializer.tsxComponente invisível que inicializa wallet (roda ao lado do AuthInitializer)
app/components/auth/LoginModal.tsxModal de login (POST /api/auth/login)
app/components/auth/RegisterModal.tsxModal de registro (POST /api/auth/register)
app/components/layout/Header.tsxAuth-aware: mostra user info ou botões
app/store/validationRuntime.tsArmazena resultado das validações
app/components/validation/ValidationBlockerOverlay.tsxOverlay bloqueante