fix(frontend): renovação automática de sessão — achado real reportado pelo usuário

Achado real: ACCESS_TOKEN_TTL = 15min e apiFetch nunca tentava nenhum refresh —
qualquer reload (ou até só navegar) depois de 15min logado batia 401 em toda Server
Component que chamasse a API, e o erro (ApiError cru, JSON da API) subia sem
tratamento nenhum até virar a tela de erro genérica do Next.js. Reportado pelo
usuário: "Ctrl+F5" depois de um tempo deu "Token de acesso inválido ou expirado".

apps/frontend/src/middleware.ts (novo) decodifica o exp do access token guardado no
cookie (só leitura, sem verificar assinatura — isso continua sendo a API) e, se
faltar menos de 60s pra vencer, chama POST /auth/refresh antes da Server Component
rodar, gravando o cookie novo tanto na resposta quanto na própria requisição (padrão
documentado do Next.js — sem isso a MESMA requisição que disparou o refresh ainda
leria o cookie velho).

Rede de segurança pro caso do refresh token também ter vencido/sido revogado:
apps/frontend/src/app/error.tsx detecta uma mensagem de sessão expirada, desloga e
manda pro /login sozinho, em vez de mostrar a tela crua de erro. Tem que ficar na
raiz de app/, não dentro de app/app/ ou app/platform/ — error.tsx nunca pega erro do
próprio layout do mesmo segmento (achado testando o primeiro corte).

Testado ponta a ponta com refresh token de verdade: token forjado pra vencer em 20s
+ refresh token real → renovação automática confirmada, página protegida carrega
normal. Com refresh token inválido de propósito → confirmado o fallback: error
boundary detecta, desloga e redireciona pro login sozinho.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 11:18:24 -03:00
parent a15ea26cb5
commit 7bc4b2e819
4 changed files with 189 additions and 0 deletions

40
TODO.md
View File

@@ -1788,6 +1788,46 @@ secao 96-103, 124, 169) + achado real de autorização em `/ai/models`
mobile — logos aparecendo corretos em todos, sem o artefato de
retângulo branco no Handix
## PHASE 51 — Renovação automática de sessão (achado real, reportado pelo
usuário: `Ctrl+F5` depois de um tempo logado quebrava com 401 cru)
- [x] achado real: `ACCESS_TOKEN_TTL = "15m"` (secao 148) e `apiFetch`
nunca tentava nenhum refresh — qualquer reload (ou até só navegar)
depois de 15min logado batia 401 em toda Server Component que
chamasse a API, e o erro (`ApiError` cru, JSON da API) subia sem
tratamento nenhum até virar a tela de erro genérica do Next.js. Não
era um caso raro: qualquer sessão de teste mais longa que 15min
batia nisso
- [x] `apps/frontend/src/middleware.ts` (novo) — decodifica o `exp` do
access token guardado no cookie (sem verificar assinatura, só
leitura — a verificação de verdade continua sendo feita pela API) e,
se faltar menos de 60s pra vencer, chama `POST /auth/refresh` com o
refresh token *antes* da Server Component rodar, gravando o cookie
novo tanto na resposta (`response.cookies`) quanto na própria
requisição (`request.cookies`, via `NextResponse.next({ request })`)
— sem isto, a MESMA requisição que disparou o refresh ainda leria o
cookie velho (padrão documentado do Next.js pra refresh de auth em
middleware que precisa valer já na requisição atual, não só na
próxima)
- [x] Rede de segurança pro caso do refresh token TAMBÉM já ter vencido/
sido revogado (sessão parada por mais de 30 dias, ou logout em outro
lugar): `apps/frontend/src/app/error.tsx` (Error Boundary do App
Router) detecta uma mensagem de sessão expirada/401, desloga
(`POST /api/logout`) e manda pro `/login` sozinho, em vez de mostrar
a tela crua "Application error: a server-side exception...". Tem
que ficar na raiz de `app/`, não dentro de `app/app/` ou
`app/platform/` — `error.tsx` nunca pega erro do PRÓPRIO layout do
mesmo segmento (é o layout de `/app`/`/platform` que chama
`/auth/me` e lança o erro), só de layouts/páginas aninhados abaixo
(achado testando: o primeiro corte com um `error.tsx` por segmento
não pegava nada, confirmado só depois de mover pra raiz)
- [x] Testado ponta a ponta com refresh token de verdade: token forjado
pra vencer em 20s + refresh token real → renovação automática
confirmada (token novo no cookie, página protegida carrega normal,
sem nenhum 401 visível). Com refresh token inválido de propósito →
confirmado o fallback: erro 500 na resposta inicial (esperado, SSR),
mas o error boundary do client detecta, desloga e redireciona pro
login sozinho — sem crash na tela pro usuário
---
## Riscos conhecidos