Tema
Ciclo de Requisição NestJS
Toda chamada HTTP ao backend percorre um pipeline fixo e determinístico. Entender este ciclo é essencial para debugar problemas de autenticação, autorização e contexto de tenant.
Visão geral do pipeline
HTTP Request (cliente/frontend)
│
▼
1. SETUP (main.ts)
├── Trust Proxy = 1
├── Helmet (headers de segurança)
├── rawBody capture (HMAC webhooks)
├── CORS dinâmico
└── GlobalValidationPipe
│
▼
2. MIDDLEWARE (executa em todo request)
├── CorrelationIdMiddleware → gera/propaga x-correlation-id
└── HttpLoggerMiddleware → loga método/url/status/duração
│
▼
3. GUARDS (execução em ordem, primeiro que falhar bloqueia)
├── UserThrottlerGuard → rate limit por user ID ou IP
├── ClerkAuthGuard → valida JWT do Clerk, auto-sync ao Prisma
├── RolesGuard → verifica decorator @Roles
├── PermissionsGuard → verifica decorator @Permissions
└── ReadOnlyGuard → bloqueia POST/PUT/PATCH/DELETE se role.readOnly
│
▼
4. INTERCEPTORS (antes do handler)
├── TenantInterceptor → popula AsyncLocalStorage com organizationId
└── AuditInterceptor → registra ação na tabela accessLog
│
▼
5. CONTROLLER METHOD (handler)
└── @CurrentUser() → req.user injetado
│
▼
6. SERVICE LAYER
├── Lógica de negócio
├── EncryptionService (AES-256-GCM)
├── MailService (SMTP via Nodemailer)
├── MinioService (upload S3-compatible)
└── BullMQ Queue (jobs assíncronos)
│
▼
7. REPOSITORY / PRISMA
└── PostgreSQL (com organizationId do tenant context)
│
▼
8. RESPONSE
├── AuditInterceptor tapa a resposta → cria AccessLog
└── x-correlation-id retornado no headerDetalhe por etapa
1. main.ts — Bootstrap
Arquivo: backend/src/main.ts
typescript
// Middlewares Express na ordem exata:
app.set('trust proxy', 1) // IP real atrás do Traefik
app.use(helmet()) // CSP, HSTS, etc.
app.use(json({ verify: rawBodyCapture })) // rawBody para HMAC
app.use(urlencoded({ extended: true }))
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip campos não declarados no DTO
forbidNonWhitelisted: true, // erro se campo extra
transform: true, // cast automático de tipos
}))CORS dinâmico: permite https://*.galdtech.com, https://galdix.com.br, e domínios listados em ALLOWED_ORIGINS.
2. Middlewares
CorrelationIdMiddleware
Gera UUID para cada request se x-correlation-id não estiver presente. Disponibiliza em req.correlationId e retorna no response header.
HttpLoggerMiddleware
Loga ao final de cada request:
GET /webhook/erp/customers 200 45ms | user=a1b2c3d4e5f6 org=org-uuid role=1 | 192.168.x.x Mozilla/5.0- Pseudonimização: userId/clerkId/email são hasheados (SHA-256, 12 chars hex)
- Mascaramento de IP: IPv4 →
x.x.x.*, IPv6 → primeiros 4 grupos +****
3. Guards
Registrados globalmente no AppModule como APP_GUARD, executam na ordem registrada.
UserThrottlerGuard
- Chave:
user-{id}se autenticado, IP se anônimo - Limite default: 100 requisições / 60 segundos (configurável via
RATE_LIMITenv) - Resposta:
429 Too Many Requests
ClerkAuthGuard
1. Rota marcada com @Public()? → passa
2. Rota /admin/queues? → passa (tem basic-auth próprio)
3. Extrai Authorization: Bearer <token>
4. clerkClient.verifyToken(token) → extrai sessionClaims.sub (Clerk user ID)
5. prisma.user.findUnique({ clerkId: sub })
6. Não encontrou? → chama clerkClient.users.getUser() → syncUser() → cria no DB
7. Injeta request.user = dbUser (com role, stores, organizationId)
8. Retorna true (passa)Erros:
- Sem token →
401 Unauthorized - Token inválido →
401 Unauthorized - Clerk retorna 403 → re-throw
RolesGuard
- Lê metadata
@Roles(roleId1, roleId2)do handler role.level === 0(Super Admin) → passa sempre- Verifica se
user.role.idestá na lista
PermissionsGuard
- Lê metadata
@Permissions('app:campaigns', ...)do handler role.level === 0→ passa sempre- Requer que pelo menos uma das permissions esteja em
user.role.permissions
ReadOnlyGuard
- Se
role.readOnly === truee método éPOST/PUT/PATCH/DELETE→403 Forbidden role.level === 0→ passa sempre- Rotas
@Public()→ passa sempre
4. Interceptors
TenantInterceptor
typescript
// Determina organizationId ativo:
let activeOrgId = user.organizationId
// Super Admin pode override via header:
if (user.role.level === 0 && req.headers['x-org-id']) {
activeOrgId = req.headers['x-org-id'] // deve ser UUID válido
}
// Popula AsyncLocalStorage para todo código downstream:
return tenantContext.run({ organizationId: activeOrgId, role }, () =>
next.handle()
)Todos os serviços podem acessar tenantContext.getStore() sem precisar receber organizationId como parâmetro.
AuditInterceptor
Registra ações em accessLog baseado em uma allowlist de regras (método + regex de rota + actionType):
| Regra | Action Type |
|---|---|
POST /users | USER_INVITE |
PATCH /users/:id/role | USER_ROLE_CHANGE |
DELETE /users/:id | USER_DELETE |
POST /campaigns | CAMPAIGN_CREATE |
POST /campaigns/:id/send | CAMPAIGN_SEND |
POST /sales/trigger-sync | SYNC_TRIGGERED |
| (e mais 15+ regras) | ... |
Rotas que não casam com nenhuma regra são ignoradas silenciosamente.
5. Controller
typescript
@Controller('campaigns')
@Permissions('app:campaigns')
export class CampaignsController {
@Post()
@Permissions('app:campaigns:send')
async create(
@CurrentUser() user: AuthedUser, // injetado pelo guard
@Body() dto: CreateCampaignDto, // validado pelo ValidationPipe
) {
return this.campaignsService.create(dto, user)
}
}O decorator @CurrentUser() extrai req.user (populado pelo ClerkAuthGuard).
6. Tipo AuthedRequest
typescript
interface AuthedRequest extends Request {
user: {
id: string
clerkId: string
email: string
name: string
organizationId: string
storeId?: string | null
role?: {
id: string
name: string
level: number // 0 = Super Admin
permissions: string[]
readOnly: boolean
} | null
stores?: Array<{ id: string; code: string }>
}
rawBody?: Buffer // para validação HMAC de webhooks
correlationId?: string
}7. Exemplo completo: POST /campaigns
1. Request chega: POST /campaigns
Headers: Authorization: Bearer eyJ...
2. Trust proxy → IP real extraído
3. Helmet → headers de segurança adicionados
4. rawBody capturado (para potencial HMAC)
5. CORS validado
6. CorrelationIdMiddleware → x-correlation-id: uuid-abc123
7. HttpLoggerMiddleware → start = Date.now()
8. UserThrottlerGuard → chave "user-uuid-xyz" → 15/100 → passa
9. ClerkAuthGuard → verifica JWT → carrega user + role do DB
10. RolesGuard → nenhum @Roles → passa
11. PermissionsGuard → @Permissions('app:campaigns') → user tem → passa
12. ReadOnlyGuard → POST, user.role.readOnly=false → passa
13. TenantInterceptor → tenantContext = { organizationId: "org-123", role: {...} }
14. AuditInterceptor → inicia tap()
15. Controller: create(user, dto)
16. Service: campaignsService.create(dto, user.organizationId)
17. Prisma: INSERT INTO campaigns(...)
18. BullMQ: enfileira job de trigger (se scheduledAt)
19. Response: { id, name, status, ... }
Status: 201 Created
Header: x-correlation-id: uuid-abc123
20. AuditInterceptor.tap() → prisma.accessLog.create({ actionType: 'CAMPAIGN_CREATE' })
21. HttpLoggerMiddleware: "POST /campaigns 201 52ms | user=a1b2... | 192.168.x.x"Tratamento de erros
| Cenário | Código | Quem lança |
|---|---|---|
| Sem token | 401 | ClerkAuthGuard |
| Token inválido | 401 | ClerkAuthGuard |
| Sem permissão | 403 | PermissionsGuard |
| Role errada | 403 | RolesGuard |
| Mutação read-only | 403 | ReadOnlyGuard |
| DTO inválido | 400 | ValidationPipe |
| Rate limit | 429 | UserThrottlerGuard |
| Recurso não encontrado | 404 | Service (NotFoundException) |
| Erro interno | 500 | NestJS exception filter |
Webhooks (fluxo diferente)
Rotas de webhook usam @Public() para pular o ClerkAuthGuard. A autenticação é feita por:
- Meta WhatsApp: HMAC-SHA256 do rawBody contra
APP_SECRET - Clerk: Svix signature (
svix-id,svix-timestamp,svix-signature) - Chatwoot: query param
?secret=CHATWOOT_WEBHOOK_SECRET - n8n:
Authorization: Bearer INTERNAL_API_KEY