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 que | Fonte reativa correta |
|---|---|
isAuthenticated, user, userInfo | useAccountsStore |
| Wallet, transações | useAccountsStore (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
isAuthenticatedvem deuseAccountsStore, não deuseRouteLoaderData? - Há fallback pra SSR via
authHydrated(se zero-flash importa)? - Não há cache adicional de
user.idem ref/state que ignora mudanças? - Se o componente dispara side-effects (ex: carregar dados do usuário), recomputa quando
user?.idmuda?
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):
- force —
forceRequestKyc = trueno perfil (backend forçou KYC) - regulatory —
showPendingDataFlow = trueno perfil (dados regulatórios pendentes, ex: endereço) - global —
config.global.active = truee 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
| Arquivo | Papel |
|---|---|
app/store/auth.ts | Zustand store: user, userInfo, isAuthenticated, hydrated, authModal + clearAuth (limpa wallet) |
app/store/wallet.ts | Zustand store: wallet state + clearWallet (chamado pelo clearAuth) |
app/services/auth.client.ts | Singleton AuthService + ApiClient client-side |
app/utils/cookie.server.ts | getTokenFromRequest, makeSetTokenHeader, makeDeleteTokenHeader |
app/hooks/useAuthInit.ts | Sync do auth do loader SSR para store (NÃO chama initAuthService) |
app/hooks/useAuthProfileSync.ts | Sync de profile atualizado |
app/hooks/useValidationRuntimeSync.ts | Computa allValidations e sincroniza com store |
app/components/auth/AuthInitializer.tsx | Componente invisível que chama useAuthInit + useAuthProfileSync + useValidationRuntimeSync |
app/components/wallet/WalletInitializer.tsx | Componente invisível que inicializa wallet (roda ao lado do AuthInitializer) |
app/components/auth/LoginModal.tsx | Modal de login (POST /api/auth/login) |
app/components/auth/RegisterModal.tsx | Modal de registro (POST /api/auth/register) |
app/components/layout/Header.tsx | Auth-aware: mostra user info ou botões |
app/store/validationRuntime.ts | Armazena resultado das validações |
app/components/validation/ValidationBlockerOverlay.tsx | Overlay bloqueante |