feat(signatures): UI completa do módulo + rate limiting em rotas públicas
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>
This commit is contained in:
120
apps/api/src/common/rate-limit/rate-limit-rules.ts
Normal file
120
apps/api/src/common/rate-limit/rate-limit-rules.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { RateLimitRule } from "./rate-limit.decorator.js";
|
||||
|
||||
const MINUTE = 60_000;
|
||||
|
||||
/**
|
||||
* Concrete limits for every public (no-session) route in this API.
|
||||
* Numbers are deliberately round, chosen for two constraints at once:
|
||||
* - generous enough that this project's own real-app integration test
|
||||
* suite (apps/api/test/*.spec.ts — real login/refresh/signature flows,
|
||||
* all from the same loopback IP) never trips them by accident;
|
||||
* - tight enough to turn an unthrottled endpoint (previously: unlimited
|
||||
* requests/second) into a hard, auditable ceiling per IP or per link.
|
||||
* None of these claim to fully stop a determined, IP-rotating attacker —
|
||||
* that's a documented residual risk in docs/security/threat-model.md,
|
||||
* not a gap this change pretends to close.
|
||||
*/
|
||||
|
||||
// --- apps/api/src/auth/auth.controller.ts -----------------------------
|
||||
|
||||
/** Credential guessing target — argon2 already raises the per-guess cost; this caps guess *rate*. */
|
||||
export const AUTH_LOGIN_RULE: RateLimitRule = { name: "auth-login", keyBy: "ip", limit: 10, windowMs: MINUTE };
|
||||
|
||||
/** Same order of magnitude as login: this is the second half of an MFA login, equally sensitive. */
|
||||
export const AUTH_MFA_CHALLENGE_RULE: RateLimitRule = {
|
||||
name: "auth-mfa-challenge",
|
||||
keyBy: "ip",
|
||||
limit: 10,
|
||||
windowMs: MINUTE,
|
||||
};
|
||||
|
||||
/** Doesn't leak account existence (anti-enumeration response either way), but still caps inbox-spam/abuse potential. */
|
||||
export const AUTH_FORGOT_PASSWORD_RULE: RateLimitRule = {
|
||||
name: "auth-forgot-password",
|
||||
keyBy: "ip",
|
||||
limit: 10,
|
||||
windowMs: MINUTE,
|
||||
};
|
||||
|
||||
/** Token is 256-bit random (auth.service.ts) — not brute-forceable; this is generic abuse/DoS hygiene. */
|
||||
export const AUTH_RESET_PASSWORD_RULE: RateLimitRule = {
|
||||
name: "auth-reset-password",
|
||||
keyBy: "ip",
|
||||
limit: 10,
|
||||
windowMs: MINUTE,
|
||||
};
|
||||
|
||||
/** Refresh tokens are 256-bit random too; higher ceiling because legitimate multi-tab/background refresh is frequent by design. */
|
||||
export const AUTH_REFRESH_RULE: RateLimitRule = { name: "auth-refresh", keyBy: "ip", limit: 30, windowMs: MINUTE };
|
||||
|
||||
/**
|
||||
* Not in the task's original enumerated list, but POST /v1/auth/logout has
|
||||
* no JwtAuthGuard either (auth.controller.ts) — genuinely public. No
|
||||
* guessing target (needs an already-known refresh token), so this is
|
||||
* generic abuse/DoS hygiene only, same ceiling as refresh.
|
||||
*/
|
||||
export const AUTH_LOGOUT_RULE: RateLimitRule = { name: "auth-logout", keyBy: "ip", limit: 30, windowMs: MINUTE };
|
||||
|
||||
// --- apps/api/src/signatures/public-signature.controller.ts -----------
|
||||
|
||||
/**
|
||||
* Shared across every /v1/public-signature/:token/* handler, keyed by IP —
|
||||
* catches one source scanning/hammering many DIFFERENT tokens (link
|
||||
* enumeration or scraping), which a per-token limit alone can't see.
|
||||
*/
|
||||
export const PUBLIC_SIGNATURE_IP_RULE: RateLimitRule = {
|
||||
name: "pubsig-ip",
|
||||
keyBy: "ip",
|
||||
limit: 60,
|
||||
windowMs: MINUTE,
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-token ceiling for the ordinary read/ack steps of the flow — a real
|
||||
* signer hits each of these once or twice (reload, double-click); this
|
||||
* only bites automated hammering of one leaked link, from any number of
|
||||
* source IPs (that's the point of keying by token, not IP, here).
|
||||
*/
|
||||
export const PUBLIC_SIGNATURE_TOKEN_ACTION_RULE: RateLimitRule = {
|
||||
name: "pubsig-token-action",
|
||||
keyBy: "token",
|
||||
limit: 20,
|
||||
windowMs: 5 * MINUTE,
|
||||
};
|
||||
|
||||
/** Sends an email — tighter, since abuse here has a real cost (spamming the signer's inbox), not just a wasted read. */
|
||||
export const PUBLIC_SIGNATURE_OTP_REQUEST_RULE: RateLimitRule = {
|
||||
name: "pubsig-otp-request",
|
||||
keyBy: "token",
|
||||
limit: 5,
|
||||
windowMs: 15 * MINUTE,
|
||||
};
|
||||
|
||||
/**
|
||||
* Complementary to the DB-persisted per-challenge lockout already in
|
||||
* public-signature.service.ts::verifyOtp (blocks after N wrong codes,
|
||||
* survives across source IPs) — this caps raw request *rate* against the
|
||||
* endpoint itself, independent of whether attempts are right or wrong.
|
||||
*/
|
||||
export const PUBLIC_SIGNATURE_OTP_VERIFY_RULE: RateLimitRule = {
|
||||
name: "pubsig-otp-verify",
|
||||
keyBy: "token",
|
||||
limit: 15,
|
||||
windowMs: 5 * MINUTE,
|
||||
};
|
||||
|
||||
// --- apps/api/src/signatures/public-verify.controller.ts --------------
|
||||
|
||||
export const PUBLIC_VERIFY_IP_RULE: RateLimitRule = {
|
||||
name: "pubverify-ip",
|
||||
keyBy: "ip",
|
||||
limit: 60,
|
||||
windowMs: MINUTE,
|
||||
};
|
||||
|
||||
export const PUBLIC_VERIFY_ID_RULE: RateLimitRule = {
|
||||
name: "pubverify-id",
|
||||
keyBy: "verificationId",
|
||||
limit: 30,
|
||||
windowMs: MINUTE,
|
||||
};
|
||||
Reference in New Issue
Block a user