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
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID (PK) | Gerado automaticamente |
usuario_id | UUID (FK → usuarios) | null para logs de sistema (crons, webhooks) |
nome_usuario | TEXT | Nome do autor da ação |
papel_principal | TEXT | Papel no momento da ação |
ip | TEXT | IP do cliente |
acao | TEXT | Formato: <modulo>.<operacao> |
detalhes | JSONB | Payload estruturado |
cidade | TEXT | Via geolocalização |
estado | TEXT | Via geolocalização |
pais | TEXT | Via geolocalização |
criado_em | TIMESTAMPTZ | Timestamp 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_ROLEvia helper)
2. Helper registrarLog()
Arquivo: supabase/functions/_shared/logging-helper.ts
Assinatura
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:
// ✅ 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ção | Logar? |
|---|---|
| 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
{
"tipo": "create",
"entidade": "escola",
"entidadeId": "uuid-aqui",
"resumo": { "nome": "...", "cidade": "...", "estado": "..." }
}UPDATE (com diff)
{
"tipo": "update",
"entidade": "escola",
"entidadeId": "uuid-aqui",
"alteracoes": [
{ "campo": "nome", "antes": "X", "depois": "Y" },
{ "campo": "status", "antes": "ativa", "depois": "encerrada" }
]
}SOFT DELETE
{
"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:
{
"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"
}
}| Campo | Fonte | Descrição |
|---|---|---|
user_agent | Header X-OLP-User-Agent (Worker) ou User-Agent (fallback) | String completa do User-Agent |
navegador | Parseado do UA | Ex: Chrome 120, Safari 17, Firefox 115 |
so | Parseado do UA | Ex: Windows 10/11, macOS 14.2, Android 14 |
dispositivo | Parseado do UA | Desktop, Mobile ou Tablet |
isp | Header 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 clienteX-OLP-ASN: Nome do ISP viarequest.cf.asOrganization(gratuito no Cloudflare)
5. Helper gerarAlteracoes() (diff)
Arquivo: supabase/functions/_shared/diff-helper.ts
Compara dois objetos e retorna apenas campos alterados:
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
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-City→cidadeX-Geo-Region→estadoX-Geo-Country→pais
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ção | Descrição |
|---|---|
login.success | Login bem-sucedido |
login.success_senha | Login por senha OK |
login.falha_senha | Senha incorreta |
login.failed | Login falho |
login.logout | Logout |
auth.senha_definida | Senha definida pela primeira vez |
auth.senha_redefinida | Senha redefinida (reset pós-OTP) |
auth.senha_alterada | Senha alterada (troca autenticada com senha atual) |
auth.bloqueio_brute_force_senha | Lockout ativado por tentativas de senha |
Escola
| Ação | Descrição |
|---|---|
escola.create | Criação |
escola.update | Atualização |
escola.deactivate | Suspensão |
escola.reactivate | Reativação |
escola.soft_delete | Encerramento |
Usuário
| Ação | Descrição |
|---|---|
usuario.create | Criação |
usuario.update | Atualização |
usuario.deactivate | Desativação |
usuario.reactivate | Reativação |
usuario.soft_delete | Exclusão |
Plano / Assinatura
| Ação | Descrição |
|---|---|
plano.create/update/deactivate/reactivate | CRUD de planos |
assinatura.create/update/suspend/reactivate/cancel | Gestão de assinaturas |
Gestor / Especialista (legacy)
| Ação | Descrição |
|---|---|
gestor.create/update/soft_delete | Usuários da escola |
especialista.create/update/soft_delete | Especialistas |
Banner / Header
| Ação | Descrição |
|---|---|
banner.create/update/activate/deactivate/delete | Banners de login |
header.create/update/activate/deactivate/delete | Headers de novidades |
Olimpíadas
| Ação | Descrição |
|---|---|
olimpiada.create/update/archive/reactivate | CRUD |
olimpiada.cronograma_save/links_save | Configuração |
olimpiada.aderir/remover_adesao | Adesão de escolas |
Templates / Cursos / Tutoriais
| Ação | Descrição |
|---|---|
template_hub.create | Hub de templates |
template.create/update/delete | Templates |
curso.create/update/delete | Cursos |
curso_video.create/update/delete/upload_thumbnail | Vídeos |
tutorial.create/update/toggle_status/delete | Tutoriais |
Alunos / Turmas / Responsáveis
| Ação | Descrição |
|---|---|
aluno.create/update/deactivate/import_batch | Gestão de alunos |
turma.create/update/delete | Turmas |
responsavel.create/update/vincular | Responsáveis |
Inscrições / Resultados
| Ação | Descrição |
|---|---|
inscricao.create_batch/create_auto_batch/update/cancel/confirm/generate_file | Inscrições |
resultado.upsert/create_batch/delete/set_nota_corte/set_premiacao/set_premiacoes_manual | Resultados |
Portal
| Ação | Descrição |
|---|---|
portal.login_aluno | Login de aluno bem-sucedido |
portal.login_responsavel | Login de responsável bem-sucedido |
portal.login_aluno_falha | Falha de login do aluno (matrícula errada, DN incorreta, OTP inválido, CPF não encontrado) |
portal.login_responsavel_falha | Falha de login do responsável (CPF não encontrado, OTP inválido/expirado/incorreto) |
portal.rate_limit_bloqueado | IP bloqueado por rate-limit ou lockout progressivo (detalhes: tipo_bloqueio, acao_bloqueada) |
portal.config_update | Atualização de configuração do portal |
Faturamento CRON
| Ação | Descrição |
|---|---|
faturamento.cron_started/cron_executado | Execução |
faturamento.link_gerado/sms_enviado/erro | Operações (nota: sms_enviado é nome técnico — canal real é WhatsApp via Wasender) |
faturamento.cron_erro/cron_alerta/cron_critico | Erros e alertas |
Manutenção CRON
| Ação | Descrição |
|---|---|
manutencao.executado | Execução completa |
manutencao.cleanup_* | Limpeza por categoria |
manutencao.check_whatsapp_session | Verificação de sessão WhatsApp |
Incidentes
| Ação | Descrição |
|---|---|
sms.envio_erro/falha_otp/falha_cobranca | Falhas de mensageria (WhatsApp via Wasender; nomes sms.* são técnicos/legado) |
pagamento.webhook_erro/preference_erro | Falhas de pagamento |
auth.login_falha_fatal/token_invalido/bloqueio_ip | Falhas de auth |
edge.erro_500/timeout | Erros de infraestrutura |
permissao.acesso_negado | Bloqueio 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 principalsupabase/functions/_shared/diff-helper.ts— Geração de diffsrc/constants/log-actions.ts— Catálogo de açõesdocs/security/RATE_LIMITS.md— Rate-limits relacionadosdocs/operations/INCIDENT_POLICY.md— Política de incidentesdocs/development/AUDIT_CHECKLIST.md— Checklist de auditoria (@audit)