Tema
Guards, Interceptors e Decorators
Documentação de referência para toda a camada comum (common/) do backend.
Guards
UserThrottlerGuard
Arquivo: common/guards/user-throttler.guard.ts
Estende ThrottlerGuard do @nestjs/throttler. Rate limiting por identidade do usuário autenticado ou IP para anônimos.
typescript
class UserThrottlerGuard extends ThrottlerGuard {
// Chave de rastreamento: user-{id} ou IP
getTracker(req: Request): string
// Gera a chave completa: tracker + suffix + name
generateKey(context, suffix, name): string
// Lança 429 Too Many Requests
throwThrottlingException(): void
}Configuração: TTL 60.000ms, limite via env RATE_LIMIT (default 100).
ClerkAuthGuard
Arquivo: modules/config/users/clerk-auth.guard.ts
Implementa CanActivate. Valida JWT do Clerk e sincroniza usuário com o banco.
typescript
class ClerkAuthGuard implements CanActivate {
constructor(
private configService: ConfigService,
private reflector: Reflector,
private prisma: PrismaService,
private usersSyncService: UsersSyncService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean>
}Fluxo interno:
- Verifica se rota tem
@Public()→ retornatrue - Verifica se rota é
/admin/queues→ retornatrue(basic-auth próprio) - Extrai
Authorization: Bearer <token> - Chama
clerkClient.verifyToken(token)→ obtémsub(Clerk user ID) prisma.user.findUnique({ where: { clerkId: sub }, include: { role, stores } })- Se não existe → busca no Clerk e executa
usersSyncService.syncUser() - Injeta em
request.user
RolesGuard
Arquivo: modules/config/users/roles.guard.ts
typescript
class RolesGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean>
}- Lê metadata via
@Roles(roleId)no handler ou na classe role.level === 0(Super Admin) → bypass total- Verifica
user.role.id in requiredRoles
PermissionsGuard
Arquivo: common/guards/permissions.guard.ts
typescript
class PermissionsGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean>
}- Lê metadata via
@Permissions('app:campaigns', ...) role.level === 0→ bypass- Requer pelo menos uma permission da lista em
user.role.permissions
Permissions existentes:
| Permission ID | Acesso |
|---|---|
app:dashboard | Dashboard e KPIs |
app:customers | Visualizar clientes |
app:customers:delete | Excluir clientes |
app:financials | Dados financeiros (LTV, receita) |
app:sales | Analytics (Varejo, Canais, Qualidade) |
app:segments | Criar e gerenciar segmentos |
app:export | Exportar CSV |
app:campaigns | Visualizar campanhas |
app:campaigns:send | Criar e disparar campanhas |
app:integrations | WhatsApp, templates, rate limit |
app:settings | Configurações do sistema |
app:team | Gerenciar usuários |
app:roles | Gerenciar perfis e permissões |
app:audit | Visualizar logs de auditoria |
app:store-aliases | Configurar aliases de lojas |
ReadOnlyGuard
Arquivo: common/guards/read-only.guard.ts
typescript
class ReadOnlyGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean>
}role.readOnly === true+ métodoPOST|PUT|PATCH|DELETE→403 Forbiddenrole.level === 0→ bypass- Rotas
@Public()→ bypass
InternalApiKeyGuard
Arquivo: common/guards/internal-api-key.guard.ts
Para rotas de integração server-to-server (n8n, automações internas).
typescript
class InternalApiKeyGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean>
}- Extrai
Authorization: Bearer <key> - Compara com
INTERNAL_API_KEYusandotimingSafeEqual(previne timing attacks) - Preenche ambos os buffers com mesmo comprimento antes da comparação
- Loga warning com IP e path em caso de falha
Interceptors
TenantInterceptor
Arquivo: common/interceptors/tenant.interceptor.ts
Popula o AsyncLocalStorage com contexto de tenant para multi-tenancy.
typescript
class TenantInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any>
}Fluxo:
typescript
// Determina o organizationId ativo
let activeOrgId = user.organizationId
// Super Admin pode sobrescrever via header para administrar outras orgs
if (user.role.level === 0 && isValidUuid(req.headers['x-org-id'])) {
activeOrgId = req.headers['x-org-id']
}
// Envolve o handler no contexto
return tenantContext.run({ organizationId: activeOrgId, role }, () =>
next.handle()
)Acesso downstream:
typescript
// Em qualquer serviço/repositório:
const ctx = tenantContext.getStore()
if (ctx) {
const { organizationId, role } = ctx
// usar organizationId para filtrar dados do tenant
}AuditInterceptor
Arquivo: common/interceptors/audit.interceptor.ts
Registra ações auditáveis de forma assíncrona (fire-and-forget).
typescript
class AuditInterceptor implements NestInterceptor {
constructor(private prisma: PrismaService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any>
// Lógica de log:
private logAccess(
user: AuthedUser,
request: Request,
statusCode: number,
errorMessage?: string,
): void // fire-and-forget, não aguarda
}Regras de auditoria (seleção):
| Método | Pattern | Action Type |
|---|---|---|
| POST | /users | USER_INVITE |
| PATCH | /users/[^/]+/role | USER_ROLE_CHANGE |
| DELETE | /users/[^/]+ | USER_DELETE |
| POST | /roles | ROLE_CREATE |
| POST | /campaigns | CAMPAIGN_CREATE |
| POST | /campaigns/[^/]+/send | CAMPAIGN_SEND |
| DELETE | /campaigns/[^/]+ | CAMPAIGN_DELETE |
| POST | /settings/whatsapp | SETTINGS_WHATSAPP_ADD |
| POST | /sales/trigger-sync | SYNC_TRIGGERED |
Log salvo:
typescript
prisma.accessLog.create({
data: {
userId, organizationId, storeId,
endpoint, method,
ip, // mascarado: IPv4 → x.x.x.*, IPv6 → ffff:ffff:****
userAgent,
status,
actionType,
metadata, // extraído por metadataFn da regra (ex: targetUserId)
}
})Decorators
@CurrentUser()
Arquivo: common/decorators/current-user.decorator.ts
typescript
// Uso no controller:
async create(@CurrentUser() user: AuthedUser) { ... }
// Implementação:
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) =>
ctx.switchToHttp().getRequest().user
)@Permissions(...permissions)
Arquivo: common/decorators/permissions.decorator.ts
typescript
// Uso:
@Permissions('app:campaigns', 'app:campaigns:send')
// Implementação:
export const Permissions = (...perms: string[]) =>
SetMetadata(PERMISSIONS_KEY, perms)@Roles(...roles)
Arquivo: common/decorators/roles.decorator.ts
typescript
// Uso:
@Roles('role-id-supervisor')
// Implementação:
export const Roles = (...roles: string[]) =>
SetMetadata(ROLES_KEY, roles)@Public()
Arquivo: common/decorators/public.decorator.ts
typescript
// Uso (endpoints sem autenticação):
@Public()
@Get('webhooks/whatsapp')
async handleWebhook() { ... }
// Implementação:
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true)Bypassa ClerkAuthGuard, ReadOnlyGuard e qualquer guard que verifique a flag.
Tenant Context (AsyncLocalStorage)
Arquivo: common/tenancy/tenant.context.ts
typescript
interface TenantContextData {
organizationId: string
role: { id: string; name: string; level: number }
}
// Instância singleton compartilhada por todo o processo:
export const tenantContext = new AsyncLocalStorage<TenantContextData>()Ciclo de vida:
- Iniciado pelo
TenantInterceptorviatenantContext.run(...) - Disponível para toda a call stack dentro do handler (incluindo serviços e repositórios)
- Limpo automaticamente quando o handler retorna
Middlewares
CorrelationIdMiddleware
typescript
class CorrelationIdMiddleware implements NestMiddleware {
use(req, res, next) {
const id = req.headers['x-correlation-id'] ?? randomUUID()
req.correlationId = id
res.setHeader('x-correlation-id', id)
next()
}
}HttpLoggerMiddleware
typescript
class HttpLoggerMiddleware implements NestMiddleware {
// Mascaramento de IPs:
maskIp(ip: string): string
// IPv4: "192.168.1.50" → "192.168.1.*"
// IPv6: "2001:db8:85a3::8a2e" → "2001:db8:85a3:0:****"
// Pseudonimização de usuário:
pseudonymizeUser(user: any): string
// Retorna os primeiros 12 chars do SHA-256(userId)
use(req, res, next) {
const start = Date.now()
res.on('finish', () => {
const ms = Date.now() - start
const level = res.statusCode >= 500 ? 'error'
: res.statusCode >= 400 ? 'warn'
: 'log'
logger[level](
`${req.method} ${req.url} ${res.statusCode} ${ms}ms | ` +
`user=${pseudonymize(user)} org=${orgId} role=${level} | ` +
`${maskIp(ip)} ${userAgent}`
)
})
next()
}
}Utilitários comuns
withRetry
Arquivo: common/utils/retry.util.ts
typescript
async function withRetry<T>(
fn: () => Promise<T>,
opts: {
attempts?: number // default: 3
delayMs?: number // default: 500
backoff?: 'exponential' | 'linear'
shouldRetry?: (err: unknown) => boolean
context?: string // para logging
}
): Promise<T>Backoff exponencial: delay * 2^(attempt-1) — ex: 500ms, 1s, 2s, 4s...
PhoneUtils
Arquivo: common/utils/phone.utils.ts
typescript
class PhoneUtils {
// Todas as variantes de busca (com/sem +55, com/sem 9º dígito):
static buildVariants(phone: string): string[]
// Normalização agressiva: sempre retorna "55" + dígitos
static normalize(phone: string): string
// Normalização conservadora: só adiciona 55 se 10-11 dígitos
static normalizeBrazilian(phone: string): string
// Exibição: "(XX) XXXXX-XXXX"
static format(phone: string): string
}CPF Utilities
Arquivo: common/utils/cpf-crypto.utils.ts
typescript
normalizeCpf(cpf: string): string // remove não-dígitos
hashCpf(cpf: string): string // SHA-256 dos dígitos
maskCpfDigits(cpf: string): string // "***.***.**-**"BRT Utilities
Arquivo: common/utils/brt.util.ts
typescript
const BRT_TO_UTC_OFFSET_MS = 10_800_000 // 3 horas em ms
nowBRT(): Date // hora atual como Date UTC (getUTCHours = hora BRT)
brtDayBoundsUtc(brtDate?): { start, end } // bounds UTC para um dia BRT
subtractBRTDays(brtDate, days): Date
brtMonthBoundsUtc(year, month): { start, end }
const BRT_HOUR_SQL: string // fragment SQL para extrair hora BRT de timestamp UTCRegra: sempre passe Date UTC para o Prisma. Para exibição, subtraia BRT_TO_UTC_OFFSET_MS.