Tema
Módulo Auditoria
Localização: backend/src/modules/audit/ e backend/src/common/interceptors/audit.interceptor.ts
Registra todas as ações sensíveis realizadas pelos usuários no sistema. A arquitetura é composta por dois componentes desacoplados: o AuditInterceptor (captura automática) e o AuditService (consulta e análise).
Arquitetura geral
Camada de captura:
common/interceptors/audit.interceptor.ts
└── AuditInterceptor (NestInterceptor global)
└── AUDIT_RULES (allowlist de endpoints auditáveis)
└── maskIp() (anonimização de IPs)
└── → grava em AccessLog via PrismaService
Camada de consulta:
modules/audit/
├── audit.controller.ts — HTTP layer (GET /audit/logs, GET /audit/stats)
└── audit.service.ts — AuditService (leitura, filtros, agregações)Princípio central: Apenas endpoints explicitamente declarados na allowlist AUDIT_RULES são auditados. Qualquer endpoint não mapeado é silenciosamente ignorado — isso evita poluição dos logs com leituras rotineiras de dados.
Classe: AuditInterceptor
Arquivo: backend/src/common/interceptors/audit.interceptor.ts
Interface NestJS: NestInterceptor
Interceptor global aplicado a todos os requests HTTP do backend. Atua na camada de resposta: após o handler executar (com sucesso ou com erro), verifica se a rota corresponde a alguma regra da allowlist e, caso positivo, grava um registro em AccessLog.
Dependências injetadas
PrismaService— escrita assíncrona emAccessLog
Constante AUDIT_RULES
Array de regras que define quais endpoints são auditáveis. Cada regra tem:
typescript
{
method: string; // Método HTTP (GET, POST, PATCH, DELETE, PUT)
pattern: RegExp; // Regex aplicada ao path sem query string
actionType: string; // Código da ação gravada em AccessLog.actionType
metadataFn?: (url: string, body?: any) => Record<string, any>;
// Extrator de metadados — opcional
}A tabela completa de regras está documentada na seção "Ações auditadas" abaixo.
Método intercept(context, next): Observable<any>
Ponto de entrada do interceptor. Executa para todo request HTTP.
Fluxo:
- Extrai
user,urlerequestdo contexto de execução - Se a URL contém
/health: passa adiante sem auditar (evita poluição por health checks) - Encadeia o handler com dois operadores RxJS:
tap()— executado após sucesso: chamalogAccess()com o status code real da respostacatchError()— executado após erro:- Se status 400: loga a lista de campos do body e a resposta de validação
- Chama
logAccess()com o status de erro e a mensagem de erro - Repropaga o erro sem modificar o fluxo original
Importante: O interceptor audita tanto requests bem-sucedidos quanto falhados (excluindo endpoints de health check). Logs de erro incluem um campo error nos metadados.
Método privado logAccess(user, request, status, errorMessage?)
Núcleo da lógica de auditoria. Determina se o request deve ser auditado e grava o registro.
Parâmetros:
user— objeto do usuário autenticado (dorequest.user)request— objeto HTTP completo do NestJSstatus— HTTP status code da respostaerrorMessage— mensagem de erro opcional (apenas em caso de falha)
Fluxo:
- Extrai
pathdorequest.originalUrlremovendo query string (split('?')[0]) - Extrai
methodem uppercase - Busca em
AUDIT_RULESa primeira regra onder.method === method && r.pattern.test(path) - Se nenhuma regra encontrada: retorna silenciosamente (sem log)
- Se a regra tem
metadataFn: executarule.metadataFn(path, request.body)para extrair metadados contextuais - Monta o objeto
metadatacombinando dados da regra e a mensagem de erro (se houver) - Chama
prisma.accessLog.create()de forma fire-and-forget (.catch()apenas loga erros de escrita sem propagar)
Campos gravados em AccessLog:
| Campo | Fonte |
|---|---|
userId | user.id |
organizationId | user.organizationId |
storeId | user.storeId (pode ser null) |
endpoint | request.originalUrl (com query string) |
method | Método HTTP em uppercase |
ip | x-forwarded-for header (primeiro IP) ou request.ip, após maskIp() |
userAgent | Header user-agent |
status | HTTP status code |
actionType | rule.actionType |
metadata | Resultado de metadataFn + campo error se falha |
Função maskIp(ip): string | null
Anonimiza endereços IP antes de gravar no banco. Implementada como função pura no topo do arquivo.
Comportamento por formato:
| Formato | Exemplo de entrada | Exemplo de saída |
|---|---|---|
| IPv4 completo | 192.168.1.100 | 192.168.1.* |
| IPv6 completo | 2001:db8:85a3:0:0:8a2e:370:7334 | 2001:db8:85a3:0:**** |
| Outros formatos | ::1 | ::1**** (primeiros 8 chars + ****) |
| Nulo/vazio | null | null |
Regex IPv4: /^(\d{1,3}\.\d{1,3}\.\d{1,3})\.\d{1,3}$/ — captura os 3 primeiros octetos e substitui o quarto por *.
IPv6: Split por :, mantém os 4 primeiros grupos, concatena ':****'.
Classe: AuditService
Arquivo: backend/src/modules/audit/audit.service.ts
Responsável pelas consultas e agregações de dados de auditoria. Todos os métodos são filtrados por organizationId — isolamento total de tenant.
Dependências injetadas
PrismaService— leitura deAccessLogeUser
Interface AuditLogsFilter
typescript
interface AuditLogsFilter {
userId?: string; // Filtro por usuário específico
actionType?: string; // Filtro por tipo de ação exato
startDate?: string; // ISO 8601 date string (ex: "2025-01-01")
endDate?: string; // ISO 8601 date string (ex: "2025-12-31")
search?: string; // Busca parcial e case-insensitive no campo endpoint
page?: number; // Página atual (padrão: 1)
limit?: number; // Registros por página (padrão: 50, máximo: 200)
}Método findAll(organizationId, filter): Promise<PaginatedLogs>
Retorna logs de auditoria paginados com filtros opcionais.
Parâmetros:
organizationId— UUID da organização (obrigatório, garante isolamento)filter— objetoAuditLogsFilter
Comportamento dos filtros:
| Filtro | Operador Prisma | Observação |
|---|---|---|
userId | where.userId = userId | Igualdade exata |
actionType | where.actionType = actionType | Igualdade exata |
search | where.endpoint = { contains, mode: 'insensitive' } | Busca parcial case-insensitive |
startDate | where.createdAt.gte = new Date(startDate + 'T00:00:00.000Z') | Início do dia em UTC |
endDate | where.createdAt.lte = new Date(endDate + 'T23:59:59.999Z') | Final do dia em UTC |
Limite de segurança: safeLimit = Math.min(Number(limit) || 50, 200) — máximo absoluto de 200 registros por página, independente do que o cliente solicite.
Paginação:
offset = (page - 1) * safeLimit- Executa
countefindManyem paralelo viaPromise.all() include: { user: { select: { id, name, email } } }— dados básicos do usuário vêm junto
Resposta:
typescript
{
data: AccessLog[], // Array de logs com dados do usuário aninhados
pagination: {
total: number, // Total de registros que satisfazem os filtros
page: number, // Página atual
totalPages: number, // Math.ceil(total / safeLimit)
}
}Ordenação: createdAt DESC — mais recentes primeiro.
Tabelas afetadas: AccessLog (leitura), User (leitura via include)
Método getStats(organizationId, startDate?, endDate?): Promise<AuditStats>
Retorna estatísticas agregadas de auditoria para o período especificado. Executa 3 queries em paralelo via Promise.all().
Parâmetros:
organizationId— UUID da organizaçãostartDate— início do período (opcional)endDate— fim do período (opcional)
Queries executadas em paralelo:
1. totalLogs
AccessLog.count({ where })— total de eventos no período
2. actionTypeCounts
AccessLog.groupBy({ by: ['actionType'], _count })— contagem por tipo de ação- Ordenado por
_count DESC(ações mais frequentes primeiro)
3. uniqueUsersGroups
AccessLog.groupBy({ by: ['userId'], _count, take: 5 })— top 5 usuários mais ativos- Ordenado por
_count DESC
Após as queries paralelas:
- Busca detalhes dos top 5 usuários:
User.findMany({ where: { id: { in: topUserIds } } }) - Calcula total de usuários únicos:
AccessLog.groupBy({ by: ['userId'], where })e conta o array resultante
Resposta:
typescript
{
totalLogs: number, // Total de eventos no período
uniqueUsers: number, // Total de usuários únicos com eventos
actionTypeDistribution: Array<{ // Distribuição por tipo de ação
actionType: string,
count: number,
}>,
topUsers: Array<{ // Top 5 usuários mais ativos
userId: string,
userName: string | null,
userEmail: string,
count: number,
}>,
}Tabelas afetadas: AccessLog (3 queries de leitura/agregação), User (leitura de detalhes dos top 5)
Classe: AuditController
Arquivo: backend/src/modules/audit/audit.controller.ts
Permissão: app:audit (aplicada a nível de controller — todos os endpoints exigem esta permissão)
Base path: /audit
GET /audit/logs
Lista logs de auditoria com filtros via query string.
Query params:
| Param | Tipo | Padrão | Descrição |
|---|---|---|---|
page | string (number) | 1 | Página de resultados |
limit | string (number) | 50 | Registros por página (máx. 200) |
userId | string | — | Filtra por UUID do usuário |
actionType | string | — | Filtra por tipo de ação |
startDate | string | — | Data inicial (YYYY-MM-DD) |
endDate | string | — | Data final (YYYY-MM-DD) |
search | string | — | Busca no campo endpoint |
Isolamento: organizationId é extraído de req.user — nunca aceito como parâmetro do cliente.
GET /audit/stats
Retorna KPIs agregados para o período.
Query params:
| Param | Tipo | Descrição |
|---|---|---|
startDate | string | Data inicial (YYYY-MM-DD) |
endDate | string | Data final (YYYY-MM-DD) |
Ações auditadas
Usuários e equipe
actionType | Método | Path regex | Metadados capturados |
|---|---|---|---|
USER_INVITE | POST | /users/invite$ | { email, roleId } — do body |
USER_ROLE_CHANGE | PATCH | /users/[^/]+/role$ | { targetUserId } — do path |
USER_DELETE | DELETE | /users/[^/]+$ | { targetUserId } — do path |
USER_RESET_PASSWORD | POST | /users/[^/]+/reset-password$ | { targetUserId } — do path |
Papéis (cargos)
actionType | Método | Path regex | Metadados capturados |
|---|---|---|---|
ROLE_CREATE | POST | /roles$ | — (sem metadados) |
ROLE_UPDATE | PATCH | /roles/[^/]+$ | { roleId } — do path |
ROLE_DELETE | DELETE | /roles/[^/]+$ | { roleId } — do path |
Clientes
actionType | Método | Path regex | Metadados capturados |
|---|---|---|---|
CUSTOMER_VIEW | GET | /webhook/erp/customers/[^/]+$ | { customerId } — do path |
Campanhas
actionType | Método | Path regex | Metadados capturados |
|---|---|---|---|
CAMPAIGN_CREATE | POST | /campaigns$ | — (sem metadados) |
CAMPAIGN_SEND | POST | /campaigns/[^/]+/send$ | { campaignId } — do path |
CAMPAIGN_UPDATE | PATCH | /campaigns/[^/]+$ | { campaignId } — do path |
CAMPAIGN_DELETE | DELETE | /campaigns/[^/]+$ | { campaignId } — do path |
Segmentos
actionType | Método | Path regex | Metadados capturados |
|---|---|---|---|
SEGMENT_CREATE | POST | /intelligence/segments$ | — (sem metadados) |
SEGMENT_EDIT | PUT | /intelligence/segments/[^/]+$ | { segmentId } — do path |
SEGMENT_STATUS_CHANGE | PATCH | /intelligence/segments/[^/]+/status$ | { segmentId } — do path |
SEGMENT_DELETE | DELETE | /intelligence/segments/[^/]+$ | { segmentId } — do path |
SEGMENT_EXPORT | GET | /intelligence/segments/[^/]+/export$ | { segmentId } — do path |
Configurações
actionType | Método | Path regex | Metadados capturados |
|---|---|---|---|
SETTINGS_STORE_UPDATE | POST | /settings/store$ | — (sem metadados) |
SETTINGS_EMAIL_UPDATE | POST | /settings/email$ | — (sem metadados) |
SETTINGS_WHATSAPP_ADD | POST | /settings/whatsapp$ | — (sem metadados) |
SETTINGS_WHATSAPP_DELETE | DELETE | /settings/whatsapp/[^/]+$ | { instanceId } — do path |
Sync manual (ERP)
actionType | Método | Path regex | Metadados capturados |
|---|---|---|---|
SYNC_TRIGGERED | POST | /erp/sync/sales$ | — (sem metadados) |
Schema do AccessLog
prisma
model AccessLog {
id String @id @default(uuid())
userId String
organizationId String
storeId String?
endpoint String
method String
ip String
userAgent String?
status Int
actionType String
metadata Json?
createdAt DateTime @default(now())
}Observações sobre os campos:
| Campo | Descrição |
|---|---|
id | UUID gerado pelo banco |
userId | ID do usuário que realizou a ação (referência a User.id) |
organizationId | Tenant do usuário — usado para isolamento em consultas |
storeId | Loja associada à sessão do usuário (pode ser nulo) |
endpoint | URL completa com query string (request.originalUrl) |
method | Método HTTP em uppercase |
ip | IP mascarado (maskIp()) |
userAgent | String do browser/cliente |
status | HTTP status code (200, 201, 400, 403, 404, 500...) |
actionType | Código da ação (ex: USER_INVITE, CAMPAIGN_SEND) |
metadata | JSON livre com IDs contextuais — nunca contém PII |
createdAt | Timestamp de criação (índice implícito para range queries) |
Políticas de privacidade nos logs
Mascaramento de IP
O IP é mascarado antes de ser gravado no banco, garantindo que nenhum IP completo seja armazenado:
- IPv4:
192.168.1.100→192.168.1.*(último octeto removido) - IPv6:
2001:db8:85a3:0000:0000:8a2e:0370:7334→2001:db8:85a3:0000:****
O IP original nunca é persistido. A extração considera o header x-forwarded-for (para requests via proxy/load balancer), usando apenas o primeiro IP da lista (IP real do cliente).
Ausência de PII nos metadados
Os campos metadata contêm apenas identificadores (UUIDs, IDs numéricos). Exemplos:
json
// USER_INVITE — nunca salva o nome ou CPF do convidado
{ "email": "usuario@empresa.com", "roleId": "uuid-do-cargo" }
// USER_ROLE_CHANGE — apenas IDs
{ "targetUserId": "uuid-do-usuario" }
// CAMPAIGN_SEND — apenas o ID da campanha
{ "campaignId": "uuid-da-campanha" }O campo email em USER_INVITE é a única exceção — gravado intencionalmente pois é necessário para auditoria de quem foi convidado. Todos os outros campos são apenas UUIDs.
Logs de erros de validação
Quando um request retorna HTTP 400, o interceptor loga adicionalmente no console (via Logger):
- Método e URL
- Lista de campos presentes no body (não os valores)
- Resposta de erro do NestJS
Isso facilita depuração de problemas de validação sem expor dados do usuário nos logs de infraestrutura.
Comportamento em casos especiais
Request de health check
URLs contendo /health são completamente ignoradas pelo interceptor — nenhuma checagem de regra é feita e nenhum log é gravado. Isso previne poluição dos logs por monitoramento frequente.
Falha na gravação do log
A escrita em AccessLog é feita com .catch() que apenas loga o erro no console do servidor. A falha de auditoria não propaga erro para o cliente — o request já foi completado com sucesso. Isso garante que uma indisponibilidade do banco de dados (ou uma constraint violation) não quebre operações legítimas do usuário.
Requests não autenticados
O interceptor verifica user?.id antes de chamar logAccess(). Requests sem usuário autenticado (que chegam antes do ClerkAuthGuard) não são auditados.
Requests com erros de domínio
Erros de negócio (403, 404, 409, etc.) também são auditados quando correspondem a regras na allowlist. O campo metadata.error contém a mensagem do erro (error?.response?.message || error?.message).
Fluxo completo: da ação do usuário ao log
1. Usuário faz PATCH /users/abc123/role
│
2. ClerkAuthGuard │ Autentica JWT, popula request.user
│
3. TenantInterceptor │ Estabelece contexto de organização
│
4. PermissionsGuard │ Valida permissão 'app:team'
│
5. UsersController │ Chama UsersService.updateRole()
│
6. UsersService │ Valida e atualiza User no banco
│
7. AuditInterceptor.tap() │ Após sucesso:
│ - path = '/users/abc123/role'
│ - regra encontrada: USER_ROLE_CHANGE
│ - metadataFn extrai: { targetUserId: 'abc123' }
│ - ip = maskIp(x-forwarded-for)
│ - AccessLog.create() fire-and-forget
│
8. Resposta enviada ao cliente (sem esperar o log gravar)Integração com outros módulos
Módulo Config (Users)
O UsersService.adminResetPassword() grava diretamente em AccessLog (sem passar pelo interceptor) para garantir o registro mesmo que o padrão de URL não corresponda à regra do interceptor:
typescript
await this.prisma.accessLog.create({
data: {
userId: currentUser.id,
organizationId: currentUser.organizationId,
storeId: currentUser.storeId,
endpoint: `/users/${targetUserId}/reset-password`,
method: 'POST',
actionType: 'ADMIN_PASSWORD_RESET',
// ip e userAgent não disponíveis neste contexto
},
});Neste caso, ip e userAgent ficam como null — a gravação direta não tem acesso ao objeto request.
Remoção de usuário
Quando um usuário é removido via UsersService.remove(), todos os seus AccessLog são deletados primeiro (antes do User.delete()) para evitar erro de foreign key:
typescript
await this.prisma.accessLog.deleteMany({ where: { userId: id } });Isso significa que logs de auditoria de um usuário deletado são permanentemente removidos.
Guia de consultas comuns
Ver todas as ações de um usuário nos últimos 30 dias
GET /audit/logs?userId={uuid}&startDate=2026-04-24&endDate=2026-05-24Ver apenas convites de usuários
GET /audit/logs?actionType=USER_INVITEBuscar por endpoint específico
GET /audit/logs?search=campaignsEstatísticas do mês atual
GET /audit/stats?startDate=2026-05-01&endDate=2026-05-31Resposta típica do GET /audit/stats
json
{
"totalLogs": 1240,
"uniqueUsers": 8,
"actionTypeDistribution": [
{ "actionType": "CAMPAIGN_UPDATE", "count": 320 },
{ "actionType": "CUSTOMER_VIEW", "count": 290 },
{ "actionType": "SEGMENT_EDIT", "count": 180 },
{ "actionType": "USER_INVITE", "count": 42 }
],
"topUsers": [
{ "userId": "uuid-1", "userName": "Ana Gerente", "userEmail": "ana@loja.com", "count": 430 },
{ "userId": "uuid-2", "userName": "Bruno Admin", "userEmail": "bruno@loja.com", "count": 310 }
]
}