Skip to content

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:

  1. Verifica se rota tem @Public() → retorna true
  2. Verifica se rota é /admin/queues → retorna true (basic-auth próprio)
  3. Extrai Authorization: Bearer <token>
  4. Chama clerkClient.verifyToken(token) → obtém sub (Clerk user ID)
  5. prisma.user.findUnique({ where: { clerkId: sub }, include: { role, stores } })
  6. Se não existe → busca no Clerk e executa usersSyncService.syncUser()
  7. 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 IDAcesso
app:dashboardDashboard e KPIs
app:customersVisualizar clientes
app:customers:deleteExcluir clientes
app:financialsDados financeiros (LTV, receita)
app:salesAnalytics (Varejo, Canais, Qualidade)
app:segmentsCriar e gerenciar segmentos
app:exportExportar CSV
app:campaignsVisualizar campanhas
app:campaigns:sendCriar e disparar campanhas
app:integrationsWhatsApp, templates, rate limit
app:settingsConfigurações do sistema
app:teamGerenciar usuários
app:rolesGerenciar perfis e permissões
app:auditVisualizar logs de auditoria
app:store-aliasesConfigurar 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étodo POST|PUT|PATCH|DELETE403 Forbidden
  • role.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_KEY usando timingSafeEqual (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étodoPatternAction Type
POST/usersUSER_INVITE
PATCH/users/[^/]+/roleUSER_ROLE_CHANGE
DELETE/users/[^/]+USER_DELETE
POST/rolesROLE_CREATE
POST/campaignsCAMPAIGN_CREATE
POST/campaigns/[^/]+/sendCAMPAIGN_SEND
DELETE/campaigns/[^/]+CAMPAIGN_DELETE
POST/settings/whatsappSETTINGS_WHATSAPP_ADD
POST/sales/trigger-syncSYNC_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 TenantInterceptor via tenantContext.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 UTC

Regra: sempre passe Date UTC para o Prisma. Para exibição, subtraia BRT_TO_UTC_OFFSET_MS.

Documentação interna — Galdix CRM