mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 18:04:55 +10:00
feat: better ratelimiting (#2520)
Replace hono-rate-limiter with a Prisma/PostgreSQL bucketed counter approach that works correctly across multiple instances without sticky sessions. - Add RateLimit model with composite PK (key, action, bucket) and atomic upsert - Create rate limit factory with window parsing, bucket computation, and fail-open - Define auth-tier and API-tier rate limit instances - Add Hono middleware, rateLimitResponse helper, and tRPC assertRateLimit helper - Wire rate limit headers through AppError constructor (was declared but never assigned) - Apply rate limits to auth routes (email-password, passkey), tRPC routes (2FA email, link org account), API routes, and file upload endpoints - Add cleanup cron job for expired rate limit rows (batched delete every 15 min) - Remove hono-rate-limiter dependency
This commit is contained in:
@@ -4,6 +4,8 @@ import { DateTime } from 'luxon';
|
||||
|
||||
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from '@documenso/lib/server-only/2fa/email/constants';
|
||||
import { send2FATokenEmail } from '@documenso/lib/server-only/2fa/email/send-2fa-token-email';
|
||||
import { assertRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
|
||||
import { request2FAEmailRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
|
||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@@ -21,6 +23,13 @@ export const accessAuthRequest2FAEmailRoute = procedure
|
||||
try {
|
||||
const { token } = input;
|
||||
|
||||
const rateLimitResult = await request2FAEmailRateLimit.check({
|
||||
ip: ctx.metadata.requestMetadata.ipAddress ?? 'unknown',
|
||||
identifier: token,
|
||||
});
|
||||
|
||||
assertRateLimit(rateLimitResult);
|
||||
|
||||
const user = ctx.user;
|
||||
|
||||
// Get document and recipient by token
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { linkOrganisationAccount } from '@documenso/ee/server-only/lib/link-organisation-account';
|
||||
import { assertRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
|
||||
import { linkOrgAccountRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
@@ -15,6 +17,13 @@ export const linkOrganisationAccountRoute = procedure
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { token } = input;
|
||||
|
||||
const rateLimitResult = await linkOrgAccountRateLimit.check({
|
||||
ip: ctx.metadata.requestMetadata.ipAddress ?? 'unknown',
|
||||
identifier: token,
|
||||
});
|
||||
|
||||
assertRateLimit(rateLimitResult);
|
||||
|
||||
await linkOrganisationAccount({
|
||||
token,
|
||||
requestMeta: ctx.metadata.requestMetadata,
|
||||
|
||||
@@ -37,7 +37,7 @@ const t = initTRPC
|
||||
.create({
|
||||
transformer: dataTransformer,
|
||||
errorFormatter(opts) {
|
||||
const { shape, error } = opts;
|
||||
const { shape, error, ctx } = opts;
|
||||
|
||||
const originalError = error.cause;
|
||||
|
||||
@@ -46,6 +46,12 @@ const t = initTRPC
|
||||
// Default unknown errors to 400, since if you're throwing an AppError it is expected
|
||||
// that you already know what you're doing.
|
||||
if (originalError instanceof AppError) {
|
||||
if (originalError.headers && ctx) {
|
||||
for (const [headerKey, headerValue] of Object.entries(originalError.headers)) {
|
||||
ctx.res.headers.append(headerKey, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
data = {
|
||||
...data,
|
||||
appError: AppError.toJSON(originalError),
|
||||
|
||||
Reference in New Issue
Block a user