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/lotus-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. force — forceRequestKyc = true no perfil (backend forçou KYC)
  2. regulatory — showPendingDataFlow = true no perfil (dados regulatórios pendentes, ex: endereço)
  3. global — config.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