Skip to content

Logs de Auditoria (Audit Log)

Consolidação de LOGS_SETUP.md + padrões de logging — Última atualização: 2026-02-23


1. Tabela logs_transacoes

Estrutura

CampoTipoDescrição
idUUID (PK)Gerado automaticamente
usuario_idUUID (FK → usuarios)null para logs de sistema (crons, webhooks)
nome_usuarioTEXTNome do autor da ação
papel_principalTEXTPapel no momento da ação
ipTEXTIP do cliente
acaoTEXTFormato: <modulo>.<operacao>
detalhesJSONBPayload estruturado
cidadeTEXTVia geolocalização
estadoTEXTVia geolocalização
paisTEXTVia geolocalização
criado_emTIMESTAMPTZTimestamp da ação

RLS

  • Admin: Pode ler todos os logs (papel_principal = 'administrador')
  • Escola/Gestor: Pode ler logs da sua escola
  • INSERT: Nenhuma policy (usa SERVICE_ROLE via helper)

2. Helper registrarLog()

Arquivo: supabase/functions/_shared/logging-helper.ts

Assinatura

typescript
interface LogTransacaoParams {
  usuarioId: string | null;   // null para logs de sistema
  nomeUsuario: string;
  papelPrincipal: string;
  ip: string | null;
  acao: string;               // modulo.operacao
  detalhes?: Record<string, any>;
  req?: Request;              // OBRIGATÓRIO para geolocalização
}

Regra Fundamental: req: req

TODA chamada a registrarLog() DEVE incluir req: req:

typescript
// ✅ CORRETO
await registrarLog({
  usuarioId: user.id,
  nomeUsuario: user.nome_completo,
  papelPrincipal: user.principal_role,
  ip: extractIP(req),
  acao: "modulo.operacao",
  detalhes: { ... },
  req: req,  // OBRIGATÓRIO
});

// ❌ ERRADO — falta req: req
await registrarLog({
  ip: extractIP(req),
  acao: "...",
  detalhes: { ... }
  // MISSING: req: req
});

Quando Logar

OperaçãoLogar?
CREATE✅ SIM
UPDATE✅ SIM (com diff)
DELETE / soft-delete✅ SIM
LOGIN / LOGOUT✅ SIM
SELECT / READ❌ NÃO (trade-off de performance)

3. Formato do Campo acao

<modulo>.<operacao>

Sempre em lowercase. Exemplos: login.success, escola.create, usuario.update, banner.soft_delete.


4. Estrutura do Campo detalhes (JSONB)

CREATE

json
{
  "tipo": "create",
  "entidade": "escola",
  "entidadeId": "uuid-aqui",
  "resumo": { "nome": "...", "cidade": "...", "estado": "..." }
}

UPDATE (com diff)

json
{
  "tipo": "update",
  "entidade": "escola",
  "entidadeId": "uuid-aqui",
  "alteracoes": [
    { "campo": "nome", "antes": "X", "depois": "Y" },
    { "campo": "status", "antes": "ativa", "depois": "encerrada" }
  ]
}

SOFT DELETE

json
{
  "tipo": "soft_delete",
  "entidade": "escola",
  "entidadeId": "uuid-aqui",
  "statusAnterior": "ativa",
  "statusNovo": "encerrada"
}

Metadados de Dispositivo e Rede (meta)

Quando req é passado ao registrarLog(), o helper automaticamente extrai e injeta um campo meta dentro de detalhes:

json
{
  "tipo": "login",
  "entidade": "auth",
  "meta": {
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
    "navegador": "Chrome 120",
    "so": "Windows 10/11",
    "dispositivo": "Desktop",
    "isp": "BRISANET SERVICOS DE TELECOMUNICACOES S.A"
  }
}
CampoFonteDescrição
user_agentHeader X-OLP-User-Agent (Worker) ou User-Agent (fallback)String completa do User-Agent
navegadorParseado do UAEx: Chrome 120, Safari 17, Firefox 115
soParseado do UAEx: Windows 10/11, macOS 14.2, Android 14
dispositivoParseado do UADesktop, Mobile ou Tablet
ispHeader X-OLP-ASN (Worker via request.cf.asOrganization)Nome do ISP/ASN do provedor

Headers injetados pelo Cloudflare Worker:

  • X-OLP-User-Agent: User-Agent original do cliente
  • X-OLP-ASN: Nome do ISP via request.cf.asOrganization (gratuito no Cloudflare)

5. Helper gerarAlteracoes() (diff)

Arquivo: supabase/functions/_shared/diff-helper.ts

Compara dois objetos e retorna apenas campos alterados:

typescript
import { gerarAlteracoes } from '../_shared/diff-helper.ts';

// 1. Buscar ANTES do update
const { data: antes } = await supabase.from("tabela").select("*").eq("id", id).single();

// 2. Aplicar update
const { data: depois } = await supabase.from("tabela").update(dados).eq("id", id).select().single();

// 3. Gerar diff (ignora id, criado_em, atualizado_em por padrão)
const alteracoes = gerarAlteracoes(antes, depois);

// 4. Logar
await registrarLog({
  ...params,
  acao: "entidade.update",
  detalhes: { tipo: "update", entidade: "...", entidadeId: id, alteracoes },
  req: req,
});

Interface

typescript
interface Alteracao {
  campo: string;
  antes: any;
  depois: any;
}

function gerarAlteracoes(
  anterior: Record<string, any>,
  novo: Record<string, any>,
  camposIgnorados?: string[]  // default: ["id", "criado_em", "atualizado_em", "criada_em"]
): Alteracao[]

6. Geolocalização

Resolução

Geolocalização é resolvida exclusivamente via headers nativos do Cloudflare Worker:

  • X-Geo-Citycidade
  • X-Geo-Regionestado
  • X-Geo-Countrypais

Extraídos por extractGeoFromHeaders(req) em logging-helper.ts. Sem fallback externo — se o Worker não injetar os headers, os campos ficam null.

Requisito: req: req deve ser passado em toda chamada de registrarLog().


7. Catálogo de Ações de Log

Fonte: src/constants/log-actions.ts (~100 ações)

Login/Logout

AçãoDescrição
login.successLogin bem-sucedido
login.success_senhaLogin por senha OK
login.falha_senhaSenha incorreta
login.failedLogin falho
login.logoutLogout
auth.senha_definidaSenha definida pela primeira vez
auth.senha_redefinidaSenha redefinida (reset pós-OTP)
auth.senha_alteradaSenha alterada (troca autenticada com senha atual)
auth.bloqueio_brute_force_senhaLockout ativado por tentativas de senha

Escola

AçãoDescrição
escola.createCriação
escola.updateAtualização
escola.deactivateSuspensão
escola.reactivateReativação
escola.soft_deleteEncerramento

Usuário

AçãoDescrição
usuario.createCriação
usuario.updateAtualização
usuario.deactivateDesativação
usuario.reactivateReativação
usuario.soft_deleteExclusão

Plano / Assinatura

AçãoDescrição
plano.create/update/deactivate/reactivateCRUD de planos
assinatura.create/update/suspend/reactivate/cancelGestão de assinaturas

Gestor / Especialista (legacy)

AçãoDescrição
gestor.create/update/soft_deleteUsuários da escola
especialista.create/update/soft_deleteEspecialistas
AçãoDescrição
banner.create/update/activate/deactivate/deleteBanners de login
header.create/update/activate/deactivate/deleteHeaders de novidades

Olimpíadas

AçãoDescrição
olimpiada.create/update/archive/reactivateCRUD
olimpiada.cronograma_save/links_saveConfiguração
olimpiada.aderir/remover_adesaoAdesão de escolas

Templates / Cursos / Tutoriais

AçãoDescrição
template_hub.createHub de templates
template.create/update/deleteTemplates
curso.create/update/deleteCursos
curso_video.create/update/delete/upload_thumbnailVídeos
tutorial.create/update/toggle_status/deleteTutoriais

Alunos / Turmas / Responsáveis

AçãoDescrição
aluno.create/update/deactivate/import_batchGestão de alunos
turma.create/update/deleteTurmas
responsavel.create/update/vincularResponsáveis

Inscrições / Resultados

AçãoDescrição
inscricao.create_batch/create_auto_batch/update/cancel/confirm/generate_fileInscrições
resultado.upsert/create_batch/delete/set_nota_corte/set_premiacao/set_premiacoes_manualResultados

Portal

AçãoDescrição
portal.login_alunoLogin de aluno bem-sucedido
portal.login_responsavelLogin de responsável bem-sucedido
portal.login_aluno_falhaFalha de login do aluno (matrícula errada, DN incorreta, OTP inválido, CPF não encontrado)
portal.login_responsavel_falhaFalha de login do responsável (CPF não encontrado, OTP inválido/expirado/incorreto)
portal.rate_limit_bloqueadoIP bloqueado por rate-limit ou lockout progressivo (detalhes: tipo_bloqueio, acao_bloqueada)
portal.config_updateAtualização de configuração do portal

Faturamento CRON

AçãoDescrição
faturamento.cron_started/cron_executadoExecução
faturamento.link_gerado/sms_enviado/erroOperações (nota: sms_enviado é nome técnico — canal real é WhatsApp via Wasender)
faturamento.cron_erro/cron_alerta/cron_criticoErros e alertas

Manutenção CRON

AçãoDescrição
manutencao.executadoExecução completa
manutencao.cleanup_*Limpeza por categoria
manutencao.check_whatsapp_sessionVerificação de sessão WhatsApp

Incidentes

AçãoDescrição
sms.envio_erro/falha_otp/falha_cobrancaFalhas de mensageria (WhatsApp via Wasender; nomes sms.* são técnicos/legado)
pagamento.webhook_erro/preference_erroFalhas de pagamento
auth.login_falha_fatal/token_invalido/bloqueio_ipFalhas de auth
edge.erro_500/timeoutErros de infraestrutura
permissao.acesso_negadoBloqueio de permissão

Incidentes vs. Logs

Nem todo log é um incidente. A distinção formal, critérios de classificação e convenções de nomes estão definidos em docs/operations/INCIDENT_POLICY.md.

Resumo rápido:

  • Log = registro de ação CUD normal (auditoria)
  • Incidente = falha técnica/infra/integração que requer atenção operacional
  • Ações de incidente usam sufixo .erro, .falha_*, .critico, .bloqueado

Referências

  • supabase/functions/_shared/logging-helper.ts — Helper principal
  • supabase/functions/_shared/diff-helper.ts — Geração de diff
  • src/constants/log-actions.ts — Catálogo de ações
  • docs/security/RATE_LIMITS.md — Rate-limits relacionados
  • docs/operations/INCIDENT_POLICY.md — Política de incidentes
  • docs/development/AUDIT_CHECKLIST.md — Checklist de auditoria (@audit)