Mini site de documentaçãoDeveloper Atlas

Entrada rápida para navegar arquitetura, APIs, operação e guias técnicos do projeto sem depender da estrutura do repositório.

Painel - Auth RBAC e Segurança

Esta nota explica a camada de segurança do admin como um fluxo completo.

Recorte da seçãoGuia orientado por fluxo

Leitura pensada para explicar responsabilidades, ordem de execução e trechos reais do código com foco no fluxo da implementação.

Atualizado19 de mar. de 2026
Seções26
Tags4
guiaauthrbacsegurança

O que você encontra aqui

Esta nota explica a camada de segurança do admin como um fluxo completo.

A ideia aqui é mostrar que autenticação, autorização e proteção contra abuso são camadas diferentes.

Arquivos principais

  • src/app/api/ecommpanel/auth/login/route.ts
  • src/app/api/ecommpanel/auth/login-token/request/route.ts
  • src/app/api/ecommpanel/auth/login-token/verify/route.ts
  • src/app/api/ecommpanel/auth/me/route.ts
  • src/app/api/ecommpanel/auth/logout/route.ts
  • src/app/api/ecommpanel/auth/forgot-password/route.ts
  • src/app/api/ecommpanel/auth/reset-password/route.ts
  • src/features/ecommpanel/server/auth.ts
  • src/features/ecommpanel/server/panelStore.ts
  • src/features/ecommpanel/server/rbac.ts
  • src/features/ecommpanel/server/rateLimit.ts
  • src/features/ecommpanel/server/mockStore.ts como fallback local
  • src/features/ecommpanel/config/security.ts

Persistência atual

A fase 1 de identidade do painel já funciona em dois modos:

  • PostgreSQL quando o Data Studio está com a conexão provisionada, o boilerplate aplicado e a senha do app disponível por variável de ambiente;
  • mock local como fallback quando essa conexão ainda não está pronta.

Isso significa que usuários, sessões, reset e auditoria já podem sair do mock sem quebrar o fluxo atual de desenvolvimento.

Recuperação por e-mail

O fluxo de esqueci minha senha agora trabalha em dois modos:

  • com SMTP configurado: emite token temporário e envia e-mail com link de redefinição;
  • sem SMTP: em desenvolvimento, ainda devolve token de debug para não travar a operação local.

Variáveis principais:

  • PANEL_SMTP_HOST
  • PANEL_SMTP_PORT
  • PANEL_SMTP_SECURE
  • PANEL_SMTP_USER
  • PANEL_SMTP_PASSWORD
  • PANEL_MAIL_FROM_EMAIL
  • PANEL_AUTH_BASE_URL

Além das variáveis, o painel agora expõe uma tela em Configurações do painel > Auth e e-mail para ajustar:

  • caixa remetente exibida nas mensagens;
  • usuário SMTP usado pelo auth;
  • referência da variável que guarda a senha SMTP;
  • base pública dos links de recuperação.

Importante:

  • o segredo SMTP continua vindo do ambiente;
  • a tela administrativa escolhe qual referência deve ser usada pelo runtime;
  • isso permite trocar a identidade transacional sem editar o código.

Persistência atual:

  • usuários, sessões, reset e auditoria já podem usar PostgreSQL;
  • a configuração de auth/e-mail do painel agora também pode operar em files, hybrid ou database via ECOM_PANEL_SETTINGS_PERSISTENCE_MODE.

Login alternativo por código

Além da senha, o painel agora pode enviar um código de 6 dígitos por e-mail para concluir o acesso.

Regras do fluxo:

  • o código expira em 10 minutos;
  • o código é de uso único;
  • enquanto existir um código ativo para aquele usuário, o sistema não emite outro;
  • a validação bem-sucedida cria a mesma sessão do fluxo tradicional, com cookie e CSRF.

Sessão de demonstração

O painel agora também tem um perfil de demonstração pensado para apresentação.

Regras desse perfil:

  • a tela de login já abre pré-preenchida com o acesso demo;
  • a sessão dura no máximo 30 minutos;
  • o usuário pode ser desativado ou reativado no admin;
  • o catálogo administrativo usa um sandbox por sessão, separado do estado oficial;
  • quando a sessão acaba, esse sandbox temporário é descartado.

Isso permite demonstrar a operação sem entregar poderes permanentes nem contaminar o catálogo real.

Trecho 1 - barreiras antes mesmo da senha

ts
if (!isTrustedOrigin(req)) {
  return errorNoStore(403, 'Origem não permitida.');
}

const rate = checkRateLimit(
  `auth:login:${getRequestFingerprint(req)}`,
  PANEL_SECURITY.rateLimits.login.limit,
  PANEL_SECURITY.rateLimits.login.windowMs,
);

if (!rate.allowed) {
  const response = errorNoStore(429, 'Muitas tentativas. Aguarde para tentar novamente.');
  response.headers.set('Retry-After', String(rate.retryAfterSeconds));
  return response;
}

Leitura guiada

O login não começa verificando senha.

Ele começa verificando se a requisição veio da origem esperada e se aquele cliente ainda está dentro da janela de tentativas aceitáveis.

Em termos didáticos, isso é importante porque mostra que segurança não é só criptografia. Segurança também é reduzir superfície de abuso.

Trecho 2 - verificação da conta e bloqueio temporário

ts
if (isUserLocked(user)) {
  return errorNoStore(423, 'Conta temporariamente bloqueada por segurança.');
}

const passwordMatches = await verifyPassword(password, user.passwordHash);
if (!passwordMatches) {
  const lock = await recordFailedLogin(user.id);

  if (lock.locked) {
    return errorNoStore(423, 'Conta temporariamente bloqueada por segurança.');
  }

  return errorNoStore(401, INVALID_CREDENTIALS_MESSAGE);
}

O que isso ensina

Aqui existem duas proteções diferentes:

  • o rate limit por fingerprint da requisição;
  • o bloqueio da própria conta depois de repetidas falhas.

Hoje a política está em security.ts:

  • máximo de 5 erros antes de bloquear a conta;
  • bloqueio de 15 minutos;
  • limite de 10 tentativas de login por 10 minutos no rate limiter.

Trecho 3 - criação da sessão e cookies

ts
const { session, rawSessionId } = await createSession({
  userId: user.id,
  userAgent: getUserAgent(req),
  ip: getClientIp(req),
});

const response = jsonNoStore({
  ok: true,
  user: authenticatedUser,
  csrfToken: session.csrfToken,
  mustChangePassword: authenticatedUser.mustChangePassword,
});

setAuthCookies(response, rawSessionId, session.csrfToken);

Leitura em linguagem natural

Depois que a senha passa, o sistema cria uma sessão server-side e devolve dois elementos para o cliente:

  • o cookie da sessão;
  • o token CSRF.

O ponto mais importante aqui é que a sessão persistida guarda fingerprint do ambiente de origem.

Trecho 4 - validação da sessão em cada chamada

ts
const session = options?.touch === false ? await getSession(rawSessionId) : await touchSession(rawSessionId);

if (Date.now() >= new Date(session.expiresAt).getTime()) {
  deleteSession(rawSessionId);
  return null;
}

if (!validateRequestFingerprint(req, session.userAgentHash, session.ipHash)) {
  deleteSession(rawSessionId);
  return null;
}

O que isso ensina

O cookie sozinho não basta.

A cada leitura protegida, o backend:

  1. encontra a sessão;
  2. verifica expiração;
  3. verifica se o usuário ainda está ativo;
  4. verifica o hash do user-agent;
  5. renova a janela ociosa quando faz sentido.

Isso é um bom exemplo de sessão com idle ttl e hard ttl trabalhando juntas.

Trecho 5 - RBAC real

ts
export function resolvePermissions(user: PanelUser): PanelPermission[] {
  const granted = new Set<PanelPermission>();

  for (const roleId of user.roleIds) {
    const role = PANEL_ROLES_MAP[roleId];
    if (!role) continue;
    role.permissions.forEach((permission) => granted.add(permission));
  }

  user.permissionsAllow.forEach((permission) => granted.add(permission));
  user.permissionsDeny.forEach((permission) => granted.delete(permission));

  return Array.from(granted);
}

Leitura guiada

O painel não guarda só um papel fixo e pronto.

Ele resolve permissões finais a partir de:

  • papéis base;
  • permissões extras liberadas;
  • permissões explicitamente negadas.

Isso dá mais flexibilidade e também serve muito bem para explicar a diferença entre role e permission.

Trecho 6 - autorização e CSRF nas mutações

ts
const auth = await getApiAuthContext(req);
if (!auth) return { error: errorNoStore(401, 'Não autenticado.') };
if (!hasPermission(auth.user, 'site.content.manage')) {
  return { error: errorNoStore(403, 'Sem permissão para gerenciar páginas.') };
}

if (!hasValidCsrf(req, auth.csrfToken)) {
  return errorNoStore(403, 'Token CSRF inválido.');
}

O que isso ensina

Esse é o padrão correto para toda API administrativa:

  • autenticar;
  • autorizar;
  • validar a mutação.

Se a UI esconder um botão, isso melhora usabilidade. Mas a segurança verdadeira continua sendo responsabilidade da rota.

Fluxo completo de autenticação e proteção

  1. o usuário envia email e password para /api/ecommpanel/auth/login;
  2. a rota valida origem e rate limit;
  3. a conta pode ser bloqueada se estiver em janela de abuso;
  4. a senha é verificada;
  5. o backend cria sessão, cookies e token CSRF;
  6. chamadas futuras usam a sessão para obter auth context;
  7. cada mutação exige permissão e CSRF válido;
  8. o logout revoga a sessão e limpa os cookies.

Explicando de forma simples

"No painel, estar logado não significa poder fazer tudo. Primeiro a sessão precisa existir e ser válida. Depois a role vira permissões concretas. E, por fim, cada escrita importante ainda exige origem confiável e token CSRF."

Fechamento

Se o aluno entender esta nota, ele já consegue diferenciar claramente:

  • login;
  • sessão;
  • CSRF;
  • rate limit;
  • lockout;
  • RBAC.

Camada adicional - saneamento de entrada e XSS

Além de autenticação e autorização, o sistema agora trata a entrada dos módulos mais sensíveis para reduzir risco de XSS e conteúdo malformado.

O que foi endurecido

  • links configuráveis do storefront são normalizados e protocolos perigosos são recusados;
  • campos de imagem e avatar do blog e do catálogo passam por saneamento de URL;
  • blocos editáveis do site builder sanitizam texto, links e estilos antes de persistir;
  • JSON-LD do blog usa serialização segura para não permitir quebra da tag <script>;
  • o fallback de sanitização de HTML no SSR remove tags e atributos perigosos antes de qualquer dangerouslySetInnerHTML.

Leitura prática

Isso não substitui RBAC, CSRF e sessão. É outra camada.

Mesmo um usuário autenticado pode digitar um valor ruim por acidente ou intenção. O sistema precisa impedir que esse valor vire:

  • script executável;
  • link javascript:;
  • atributo inline perigoso;
  • JSON-LD quebrado que feche a tag <script>.

Arquivos principais desta camada

  • src/utils/inputSecurity.ts
  • src/utils/sanitize.ts
  • src/features/blog/server/blogStore.ts
  • src/features/blog/components/BlogPostView.tsx
  • src/features/ecommpanel/server/siteBuilderStore.ts
  • src/features/site-runtime/storefrontTemplate.ts

Regra de arquitetura

Entrada livre pode existir.

O que não pode existir é entrada livre sendo persistida e renderizada sem passar por:

  1. normalização;
  2. validação;
  3. saneamento de saída quando necessário.