Fecha as duas pendências da leva anterior (módulo de Assinatura Eletrônica): tela de backoffice para gerenciar envelopes (criação, envio, cancelamento, checkpoint/finalização, auditoria) e as duas páginas públicas (/assinar/:token, /verificar/:verificationId). Backend: novos endpoints de streaming de PDF (documento congelado e PDF final consolidado, autenticados e via token público) que faltavam para a UI funcionar — o signatário não tinha como ver o próprio documento antes de confirmar leitura. Rate limiting (guard próprio, em memória, chaves por IP e por token) em todas as rotas sem sessão: auth (login/mfa/forgot-password/reset-password/refresh/logout) e todo o fluxo público de assinatura/verificação. Suíte: 77/77 verdes. pnpm turbo run typecheck build: 28/28. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
3.1 KiB
TypeScript
76 lines
3.1 KiB
TypeScript
import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
|
import { Reflector } from "@nestjs/core";
|
|
import type { Request, Response } from "express";
|
|
import { RATE_LIMIT_KEY, type RateLimitRule } from "./rate-limit.decorator.js";
|
|
import { RateLimiterService } from "./rate-limiter.service.js";
|
|
|
|
/**
|
|
* Enforces every `@RateLimit(...)` rule declared on a handler (public
|
|
* routes only — see auth.controller.ts / public-signature.controller.ts /
|
|
* public-verify.controller.ts). Handlers without the decorator are left
|
|
* alone (returns true immediately), so this guard is safe to attach at
|
|
* controller level without affecting authenticated routes.
|
|
*
|
|
* All rules on a handler must pass; the first one that doesn't wins the
|
|
* `Retry-After` value and gets logged. A hit here is a signal worth
|
|
* operator attention (repeated 429s from one source usually means brute
|
|
* force or scraping, not an accident), hence `warn`, not `debug`.
|
|
*/
|
|
@Injectable()
|
|
export class RateLimitGuard implements CanActivate {
|
|
private readonly logger = new Logger(RateLimitGuard.name);
|
|
|
|
constructor(
|
|
private readonly reflector: Reflector,
|
|
private readonly limiter: RateLimiterService,
|
|
) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const rules = this.reflector.get<RateLimitRule[] | undefined>(RATE_LIMIT_KEY, context.getHandler());
|
|
if (!rules || rules.length === 0) return true;
|
|
|
|
const request = context.switchToHttp().getRequest<Request>();
|
|
|
|
for (const rule of rules) {
|
|
const keyValue = this.resolveKeyValue(rule, request);
|
|
const bucketKey = `${rule.name}:${keyValue}`;
|
|
const verdict = this.limiter.consume(bucketKey, rule.limit, rule.windowMs);
|
|
|
|
if (!verdict.allowed) {
|
|
this.logger.warn(
|
|
`rate limit exceeded: rule=${rule.name} keyBy=${rule.keyBy} key=${keyValue} ip=${request.ip ?? "unknown"} route=${request.method} ${request.originalUrl}`,
|
|
);
|
|
const response = context.switchToHttp().getResponse<Response>();
|
|
response.set("Retry-After", String(verdict.retryAfterSeconds));
|
|
throw new HttpException(
|
|
{
|
|
statusCode: HttpStatus.TOO_MANY_REQUESTS,
|
|
message: "Muitas requisições. Tente novamente mais tarde.",
|
|
retryAfterSeconds: verdict.retryAfterSeconds,
|
|
},
|
|
HttpStatus.TOO_MANY_REQUESTS,
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private resolveKeyValue(rule: RateLimitRule, request: Request): string {
|
|
switch (rule.keyBy) {
|
|
case "ip":
|
|
return request.ip ?? "unknown";
|
|
case "token":
|
|
return this.stringParam(request.params.token);
|
|
case "verificationId":
|
|
return this.stringParam(request.params.verificationId);
|
|
}
|
|
}
|
|
|
|
/** @types/express v5's ParamsDictionary types values as `string | string[]` — every route here declares a single `:param`, so this is always a scalar in practice. */
|
|
private stringParam(value: string | string[] | undefined): string {
|
|
if (Array.isArray(value)) return value[0] ?? "unknown";
|
|
return value ?? "unknown";
|
|
}
|
|
}
|