Neste mini-projeto guiado, você vai finalizar um fluxo ponta a ponta de uma SPA com autenticação, adicionando permissões por role de forma consistente, aplicando ajustes finais de UX e robustez, e validando o comportamento do app do ponto de vista do usuário e do ponto de vista técnico (requisições, estados e erros). A ideia é sair de um app “funciona no happy path” para um app “resistente a cenários reais”: usuário sem permissão, token expirado, carregamento inicial, navegação direta por URL e inconsistências entre UI e API.

Objetivo do mini-projeto
Você vai implementar e validar um conjunto de regras de autorização por role em três camadas do front-end:
Rota: impedir acesso a páginas inteiras quando o usuário não tem role adequada.
Componente: esconder/mostrar botões e seções com base em roles, sem quebrar layout.
Requisição: lidar com respostas 401/403 da API e refletir isso na UI (mensagens, redirecionamentos e limpeza de sessão quando necessário).
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
Além disso, você vai fazer ajustes finais: padronização de mensagens, estados de carregamento, consistência de navegação, e uma bateria de validações ponta a ponta.
Modelo de roles e regras do app
Defina um conjunto pequeno de roles para guiar o exercício. Exemplo:
USER: pode acessar Dashboard e Perfil.
MANAGER: tudo de USER + pode ver relatórios.
ADMIN: tudo de MANAGER + pode gerenciar usuários (CRUD simplificado).
Regras de acesso sugeridas:
/app/dashboard: USER, MANAGER, ADMIN
/app/profile: USER, MANAGER, ADMIN
/app/reports: MANAGER, ADMIN
/app/admin/users: ADMIN
Regras de UI (exemplos):
Menu “Relatórios” aparece apenas para MANAGER/ADMIN.
Menu “Usuários” aparece apenas para ADMIN.
Botão “Criar usuário” aparece apenas para ADMIN.
Passo a passo: centralizando a política de autorização
Mesmo que você já tenha proteção por roles em capítulos anteriores, aqui o foco é evitar duplicação e reduzir divergências entre rota, menu e componentes. Para isso, crie uma fonte única de verdade para as regras.
1) Crie um arquivo de política (permissions)
Crie um módulo que descreve as permissões do app. A estrutura pode ser simples: um mapa de “recurso/ação” para roles permitidas, ou um mapa de rotas para roles. Exemplo híbrido (rotas + capacidades):
export const Roles = Object.freeze({ USER: 'USER', MANAGER: 'MANAGER', ADMIN: 'ADMIN'});export const routePermissions = Object.freeze({ '/app/dashboard': [Roles.USER, Roles.MANAGER, Roles.ADMIN], '/app/profile': [Roles.USER, Roles.MANAGER, Roles.ADMIN], '/app/reports': [Roles.MANAGER, Roles.ADMIN], '/app/admin/users': [Roles.ADMIN],});export const capabilities = Object.freeze({ 'reports:view': [Roles.MANAGER, Roles.ADMIN], 'users:read': [Roles.ADMIN], 'users:create': [Roles.ADMIN], 'users:update': [Roles.ADMIN], 'users:delete': [Roles.ADMIN],});Por que separar “rotas” e “capacidades”? Porque nem sempre uma permissão é 1:1 com uma rota. Por exemplo, a rota /app/admin/users pode existir para ADMIN, mas dentro dela você pode ter ações específicas (criar, editar, excluir) que dependem de capacidades.
2) Crie helpers de autorização
Centralize a lógica de “tem role?” e “tem capacidade?” para evitar ifs espalhados.
export function hasAnyRole(userRoles = [], allowedRoles = []) { if (!allowedRoles || allowedRoles.length === 0) return true; const set = new Set(userRoles); return allowedRoles.some(r => set.has(r));}export function can(userRoles = [], capabilityKey, capabilitiesMap) { const allowedRoles = capabilitiesMap[capabilityKey] || []; return hasAnyRole(userRoles, allowedRoles);}Se seu estado de autenticação expõe um único role (string), normalize para array no hook (ex.: [role]) para manter a API consistente.
Passo a passo: aplicando roles no menu e navegação
Um erro comum é proteger a rota, mas deixar o menu mostrar links que levam a 403. Isso não é “inseguro” por si só (a rota ainda bloqueia), mas é ruim para UX e gera confusão. O objetivo aqui é: menu reflete o que o usuário pode acessar.
1) Defina itens de navegação com metadados
import { Roles } from './permissions';export const navItems = [ { label: 'Dashboard', to: '/app/dashboard', roles: [Roles.USER, Roles.MANAGER, Roles.ADMIN] }, { label: 'Perfil', to: '/app/profile', roles: [Roles.USER, Roles.MANAGER, Roles.ADMIN] }, { label: 'Relatórios', to: '/app/reports', roles: [Roles.MANAGER, Roles.ADMIN] }, { label: 'Usuários', to: '/app/admin/users', roles: [Roles.ADMIN] },];2) Renderize o menu filtrando por roles
import { hasAnyRole } from './authz';import { navItems } from './nav';function AppMenu({ userRoles }) { const visible = navItems.filter(item => hasAnyRole(userRoles, item.roles)); return ( <ul> {visible.map(item => ( <li key={item.to}> <a href={item.to}>{item.label}</a> </li> ))} </ul> );}Se você usa React Router, substitua <a> por <Link> para evitar reload. O ponto aqui é o filtro por roles.
Passo a passo: páginas e componentes com autorização fina
Agora você vai criar duas páginas novas (ou completar as existentes) e aplicar autorização em nível de componente.
1) Página de Relatórios (MANAGER/ADMIN)
Crie uma página simples que consome um endpoint GET /reports/summary e mostra um card com dados. Mesmo que você não tenha API real, simule com mock e foque no fluxo de autorização e estados.

Requisitos de UX:
Mostrar loading enquanto busca.
Se receber 403, mostrar mensagem “Você não tem permissão para ver relatórios” e um link para voltar ao Dashboard.
Se receber 401, tratar como sessão inválida (ex.: redirecionar para login, dependendo da estratégia já implementada).
function ReportsPage() { const [state, setState] = React.useState({ status: 'idle', data: null, error: null }); React.useEffect(() => { let alive = true; (async () => { try { setState({ status: 'loading', data: null, error: null }); const res = await api.get('/reports/summary'); if (!alive) return; setState({ status: 'success', data: res.data, error: null }); } catch (err) { if (!alive) return; setState({ status: 'error', data: null, error: err }); } })(); return () => { alive = false; }; }, []); if (state.status === 'loading') return <p>Carregando relatórios...</p>; if (state.status === 'error') { const status = state.error?.response?.status; if (status === 403) return <p>Você não tem permissão para ver relatórios.</p>; return <p>Falha ao carregar relatórios.</p>; } return ( <div> <h2>Relatórios</h2> <pre><code>{JSON.stringify(state.data, null, 2)}</code></pre> </div> );}Observe que o componente lida com 403 mesmo que a rota já esteja protegida. Isso é útil porque: (1) permissões podem mudar no servidor, (2) o token pode ter claims desatualizadas, (3) o front-end pode estar com cache de sessão.
2) Página Admin de Usuários (ADMIN) com ações por capacidade
Crie uma tela com lista de usuários e botões condicionais. Exemplo de ações:

Listar usuários (todos ADMIN).
Criar usuário (capability
users:create).Excluir usuário (capability
users:delete).
Mesmo que todas as capacidades sejam ADMIN neste exercício, implemente como capacidades para deixar o código pronto para evoluir.
import { can } from './authz';import { capabilities } from './permissions';function UsersAdminPage({ userRoles }) { const allowCreate = can(userRoles, 'users:create', capabilities); const allowDelete = can(userRoles, 'users:delete', capabilities); const [users, setUsers] = React.useState([]); React.useEffect(() => { api.get('/admin/users').then(res => setUsers(res.data)); }, []); return ( <div> <h2>Usuários</h2> {allowCreate && <button type="button">Criar usuário</button>} <ul> {users.map(u => ( <li key={u.id}> <span>{u.email}</span> {allowDelete && ( <button type="button" onClick={() => api.delete(`/admin/users/${u.id}`)}> Excluir </button> )} </li> ))} </ul> </div> );}Aqui entram dois ajustes finais importantes:
Desabilitar durante requisições: ao excluir, desabilite o botão e trate erro 403/409/500.
Atualizar lista: após excluir, remova o item localmente ou refaça o fetch.
Passo a passo: ajustes finais de consistência do fluxo
1) Padronize o tratamento de 403 para UI
Mesmo com guard de rota, você ainda pode receber 403 em chamadas internas. Padronize um componente simples para exibir “Sem permissão” com ações.
function ForbiddenState({ message = 'Acesso negado.' }) { return ( <div> <p>{message}</p> <ul> <li><a href="/app/dashboard">Ir para o Dashboard</a></li> <li><a href="/app/profile">Ver Perfil</a></li> </ul> </div> );}Use esse componente em páginas que consomem API e podem falhar por autorização.
2) Garanta que o “usuário atual” está consistente com o token
Um problema recorrente em apps com JWT é a UI renderizar com dados de usuário “antigos” enquanto o token já mudou (por login em outra conta, refresh, ou logout). Ajustes típicos:
Ao fazer login, limpe caches de queries/dados do usuário anterior.
Ao fazer logout, limpe estado sensível (perfil, listas administrativas, etc.).
No bootstrap do app (carregamento inicial), só renderize a área autenticada após confirmar o estado de sessão.
Se você já tem um estado como auth.status (ex.: loading, authenticated, anonymous), use-o para evitar “flash” de conteúdo indevido.
3) Ajuste o comportamento ao trocar de role (cenário real)
Mesmo que o front-end não altere roles, o servidor pode revogar permissões. Você deve validar o comportamento quando:
O usuário está numa página permitida e perde permissão: próxima requisição retorna 403.
O usuário tenta navegar para uma rota agora proibida: guard deve bloquear.
Boa prática: quando receber 403 em endpoints críticos, mostre estado de “sem permissão” e ofereça navegação alternativa. Evite “deslogar” automaticamente em 403 (isso costuma ser reservado para 401/invalid session).
Validação ponta a ponta: roteiro de testes manuais
Execute o roteiro abaixo em ambiente de desenvolvimento. A intenção é validar o fluxo completo sem depender de testes automatizados (embora eles sejam recomendados).
1) Cenário: usuário USER
Faça login como USER.
Verifique que o menu mostra apenas Dashboard e Perfil.
Tente acessar diretamente
/app/reportspela URL: deve bloquear (redirecionar ou mostrar 403, conforme sua estratégia).Tente acessar diretamente
/app/admin/users: deve bloquear.No Dashboard, valide que não existe nenhum botão/atalho para áreas restritas.
2) Cenário: usuário MANAGER
Faça login como MANAGER.
Menu deve mostrar Relatórios, mas não Usuários.
Acesse Relatórios e valide loading, sucesso e tratamento de erro (simule erro 500 se possível).
Tente acessar
/app/admin/users: deve bloquear.
3) Cenário: usuário ADMIN
Faça login como ADMIN.
Menu deve mostrar Relatórios e Usuários.
Acesse Usuários: lista carrega.
Clique em “Excluir” e valide: botão desabilita durante requisição, lista atualiza após sucesso, e erros são exibidos de forma clara.
4) Cenário: token expirado durante navegação
Com usuário autenticado, force expiração (por tempo curto no backend ou alterando token no storage).
Faça uma ação que chama API (ex.: carregar relatórios).
Valide que o app não entra em loop de requisições.
Valide que a UI converge para um estado coerente: ou renova sessão (se aplicável) ou volta ao login.
5) Cenário: revogação de permissão (403) sem expirar token
Simule no backend (ou mock) que o endpoint retorna 403 para um usuário que antes tinha acesso.
Valide que a página mostra estado de “sem permissão” e não quebra o app.
Valide que o menu pode continuar exibindo o item (porque o token ainda diz que pode), mas a página deve lidar com 403. Se você quiser refinar, implemente uma estratégia de “revalidação” de permissões ao receber 403.
Validação técnica: checklist de inspeção no DevTools
1) Network: headers e status codes
Confirme que requisições autenticadas enviam o header Authorization corretamente.
Em endpoints proibidos, confirme que o servidor responde 403 (não 200 com erro no body).
Em sessão inválida, confirme 401 e o comportamento do app (logout/redirecionamento).
2) Application/Storage: consistência de sessão
Após logout, confirme que tokens e dados persistidos foram removidos.
Após login, confirme que o storage contém apenas o necessário (evite salvar payload inteiro do usuário sem necessidade).
Recarregue a página e valide que o app restaura sessão sem “piscar” conteúdo indevido.
3) Console: erros não tratados
Garanta que promessas rejeitadas estão sendo tratadas (sem “Unhandled Promise Rejection”).
Garanta que erros de API são mapeados para mensagens amigáveis (sem vazar stack trace).
Refinamentos recomendados (opcionais) para elevar a qualidade
1) Componente <RequireCapability> para ações
Para evitar repetir can(...) em muitos lugares, crie um wrapper declarativo.
function RequireCapability({ userRoles, capability, children, fallback = null }) { const allowed = can(userRoles, capability, capabilities); return allowed ? children : fallback;}Uso:
<RequireCapability userRoles={userRoles} capability="users:create"> <button type="button">Criar usuário</button></RequireCapability>2) Mapeamento de erros por status
Padronize uma função para transformar erro HTTP em “tipo de erro de UI”. Isso reduz ifs em páginas.
export function mapHttpError(err) { const status = err?.response?.status; if (status === 401) return { kind: 'unauthorized' }; if (status === 403) return { kind: 'forbidden' }; if (status === 404) return { kind: 'not_found' }; return { kind: 'unknown' };}3) Proteção contra “UI enganosa”
Se o menu é baseado apenas em roles do token, ele pode ficar desatualizado. Uma melhoria é buscar um endpoint de “me” (ex.: GET /auth/me) no bootstrap e atualizar roles do estado global. Se esse endpoint retornar 403/401, você ajusta a UI imediatamente.
Mesmo sem implementar agora, valide que o app se comporta bem quando há divergência: rota bloqueia, página trata 403, e o usuário consegue navegar para áreas permitidas.