Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Mini-projeto guiado: permissões por role, ajustes finais e validação do fluxo ponta a ponta

Capítulo 20

Tempo estimado de leitura: 11 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

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...
    Download App

    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/reports pela 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.

Agora responda o exercício sobre o conteúdo:

Qual abordagem reduz divergências entre proteção de rota, menu e botões ao implementar permissões por role em uma SPA?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Centralizar a política de autorização em um módulo de permissões e usar helpers (ex.: hasAnyRole e can) evita duplicação e inconsistências entre rota, menu e componentes, além de facilitar o tratamento de 401/403 nas requisições.

Próximo capitúlo

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.