Painel - Auth RBAC e Segurança
Esta nota explica a camada de segurança do admin como um fluxo completo.
Leitura pensada para explicar responsabilidades, ordem de execução e trechos reais do código com foco no fluxo da implementação.
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.tssrc/app/api/ecommpanel/auth/login-token/request/route.tssrc/app/api/ecommpanel/auth/login-token/verify/route.tssrc/app/api/ecommpanel/auth/me/route.tssrc/app/api/ecommpanel/auth/logout/route.tssrc/app/api/ecommpanel/auth/forgot-password/route.tssrc/app/api/ecommpanel/auth/reset-password/route.tssrc/features/ecommpanel/server/auth.tssrc/features/ecommpanel/server/panelStore.tssrc/features/ecommpanel/server/rbac.tssrc/features/ecommpanel/server/rateLimit.tssrc/features/ecommpanel/server/mockStore.tscomo fallback localsrc/features/ecommpanel/config/security.ts
Persistência atual
A fase 1 de identidade do painel já funciona em dois modos:
PostgreSQLquando oData Studioestá com a conexão provisionada, o boilerplate aplicado e a senha do app disponível por variável de ambiente;mock localcomo 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_HOSTPANEL_SMTP_PORTPANEL_SMTP_SECUREPANEL_SMTP_USERPANEL_SMTP_PASSWORDPANEL_MAIL_FROM_EMAILPANEL_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,hybridoudatabaseviaECOM_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
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
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
5erros antes de bloquear a conta; - bloqueio de
15 minutos; - limite de
10tentativas de login por10 minutosno rate limiter.
Trecho 3 - criação da sessão e cookies
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
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:
- encontra a sessão;
- verifica expiração;
- verifica se o usuário ainda está ativo;
- verifica o hash do user-agent;
- 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
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
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
- o usuário envia
emailepasswordpara/api/ecommpanel/auth/login; - a rota valida origem e rate limit;
- a conta pode ser bloqueada se estiver em janela de abuso;
- a senha é verificada;
- o backend cria sessão, cookies e token CSRF;
- chamadas futuras usam a sessão para obter
auth context; - cada mutação exige permissão e CSRF válido;
- 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.tssrc/utils/sanitize.tssrc/features/blog/server/blogStore.tssrc/features/blog/components/BlogPostView.tsxsrc/features/ecommpanel/server/siteBuilderStore.tssrc/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:
- normalização;
- validação;
- saneamento de saída quando necessário.