diff --git a/apps/www/app/api-v2/submit-form-security-newsletter/route.tsx b/apps/www/app/api-v2/submit-form-security-newsletter/route.tsx index 3e4d9ab8d71..9c45dc060f6 100644 --- a/apps/www/app/api-v2/submit-form-security-newsletter/route.tsx +++ b/apps/www/app/api-v2/submit-form-security-newsletter/route.tsx @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/nextjs' - import { CustomerioTrackClient } from '~/lib/customerio' +import { createRateLimiter } from '~/lib/rate-limit' const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -12,7 +12,16 @@ const isValidEmail = (email: string): boolean => { return emailPattern.test(email) } +const isRateLimited = createRateLimiter({ max: 5, windowMs: 60 * 1000 }) + export async function POST(req: Request) { + if (isRateLimited(req)) { + return new Response(JSON.stringify({ message: 'Too many requests. Try again later.' }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 429, + }) + } + const body = await req.json() const { firstName, lastName, email } = body diff --git a/apps/www/app/api-v2/submit-form-subprocessor-updates/route.test.ts b/apps/www/app/api-v2/submit-form-subprocessor-updates/route.test.ts new file mode 100644 index 00000000000..c8597164367 --- /dev/null +++ b/apps/www/app/api-v2/submit-form-subprocessor-updates/route.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it, vi } from 'vitest' + +import { POST } from './route' + +vi.mock('server-only', () => ({})) +vi.mock('@sentry/nextjs', () => ({ captureException: vi.fn() })) + +const makeRequest = (ip: string) => + new Request('http://localhost/api-v2/submit-form-subprocessor-updates', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-forwarded-for': ip }, + body: JSON.stringify({}), + }) + +describe('submit-form-subprocessor-updates rate limiting', () => { + it('returns 429 on the sixth request in a window from one ip', async () => { + for (let i = 0; i < 5; i++) { + const res = await POST(makeRequest('203.0.113.7')) + expect(res.status).toBe(422) + } + const res = await POST(makeRequest('203.0.113.7')) + expect(res.status).toBe(429) + }) + + it('does not rate limit a different ip', async () => { + const res = await POST(makeRequest('203.0.113.8')) + expect(res.status).toBe(422) + }) +}) diff --git a/apps/www/app/api-v2/submit-form-subprocessor-updates/route.tsx b/apps/www/app/api-v2/submit-form-subprocessor-updates/route.tsx index b9128e6e174..284b15c2852 100644 --- a/apps/www/app/api-v2/submit-form-subprocessor-updates/route.tsx +++ b/apps/www/app/api-v2/submit-form-subprocessor-updates/route.tsx @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/nextjs' - import { CustomerioTrackClient } from '~/lib/customerio' +import { createRateLimiter } from '~/lib/rate-limit' const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -12,7 +12,16 @@ const isValidEmail = (email: string): boolean => { return emailPattern.test(email) } +const isRateLimited = createRateLimiter({ max: 5, windowMs: 60 * 1000 }) + export async function POST(req: Request) { + if (isRateLimited(req)) { + return new Response(JSON.stringify({ message: 'Too many requests. Try again later.' }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 429, + }) + } + const body = await req.json() const { firstName, lastName, email } = body