From 0d24e817b9a257bf244fb8f9b921495a0e387c60 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Fri, 18 Apr 2025 10:10:07 +0530 Subject: [PATCH 1/6] Implement OTP rate limiting on the signup --- apps/web/lib/actions/create-user-account.ts | 36 +++++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/web/lib/actions/create-user-account.ts b/apps/web/lib/actions/create-user-account.ts index 8493f82c4b5..38c1080aa97 100644 --- a/apps/web/lib/actions/create-user-account.ts +++ b/apps/web/lib/actions/create-user-account.ts @@ -2,9 +2,9 @@ import { ratelimit } from "@/lib/upstash"; import { prisma } from "@dub/prisma"; +import { waitUntil } from "@vercel/functions"; import { flattenValidationErrors } from "next-safe-action"; import { createId } from "../api/create-id"; -import { getIP } from "../api/utils"; import { hashPassword } from "../auth/password"; import z from "../zod"; import { signUpSchema } from "../zod/schemas/auth"; @@ -15,6 +15,9 @@ const schema = signUpSchema.extend({ code: z.string().min(6, "OTP must be 6 characters long."), }); +const MAX_OTP_ATTEMPTS = 5; // Block after 5 failed attempts +const OTP_LOCKOUT_DURATION = "24 h"; // Block for 24 hours + // Sign up a new user using email and password export const createUserAccountAction = actionClient .schema(schema, { @@ -25,26 +28,45 @@ export const createUserAccountAction = actionClient .action(async ({ parsedInput }) => { const { email, password, code } = parsedInput; - const { success } = await ratelimit(2, "1 m").limit(`signup:${getIP()}`); + const signupAttemptKey = `signup:attempts:${email}`; + + const { remaining: attemptsRemaining } = await ratelimit( + MAX_OTP_ATTEMPTS, + OTP_LOCKOUT_DURATION, + ).getRemaining(signupAttemptKey); - if (!success) { - throw new Error("Too many requests. Please try again later."); + if (attemptsRemaining <= 0) { + throw new Error("Too many failed attempts. You have to try again later."); } const verificationToken = await prisma.emailVerificationToken.findUnique({ where: { identifier: email, token: code, - expires: { - gte: new Date(), - }, }, }); if (!verificationToken) { + await ratelimit(MAX_OTP_ATTEMPTS, OTP_LOCKOUT_DURATION).limit( + signupAttemptKey, + ); + throw new Error("Invalid verification code entered."); } + if (verificationToken.expires && verificationToken.expires < new Date()) { + waitUntil( + prisma.emailVerificationToken.delete({ + where: { + identifier: email, + token: code, + }, + }), + ); + + throw new Error("The OTP has expired. Please request a new one."); + } + await prisma.emailVerificationToken.delete({ where: { identifier: email, From e2bfc72857c79094194a8607ddf811933228a50c Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 18 Apr 2025 11:27:56 -0700 Subject: [PATCH 2/6] throw 404 if customer not found --- .../[slug]/customers/[customerId]/page-client.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx index c32bef2d761..253aa46ecbb 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx @@ -25,11 +25,7 @@ export function CustomerPageClient() { const { customerId } = useParams<{ customerId: string }>(); const { id: workspaceId, slug } = useWorkspace(); - const { - data: customer, - isLoading, - error, - } = useCustomer({ + const { data: customer, isLoading } = useCustomer({ customerId, query: { includeExpandedFields: true }, }); @@ -41,7 +37,7 @@ export function CustomerPageClient() { fetcher, ); - if (!customer && !isLoading && !error) notFound(); + if (!customer && !isLoading) notFound(); return (
From 8307ee845610f62af3034bb06578d735eca3e47e Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 18 Apr 2025 11:42:19 -0700 Subject: [PATCH 3/6] Metadata max length --- apps/web/lib/zod/schemas/leads.ts | 8 +++++++- apps/web/lib/zod/schemas/sales.ts | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/web/lib/zod/schemas/leads.ts b/apps/web/lib/zod/schemas/leads.ts index 0f38f699b37..86ad7367132 100644 --- a/apps/web/lib/zod/schemas/leads.ts +++ b/apps/web/lib/zod/schemas/leads.ts @@ -65,7 +65,13 @@ export const trackLeadRequestSchema = z.object({ .record(z.unknown()) .nullish() .default(null) - .describe("Additional metadata to be stored with the lead event"), + .transform((val) => (val ? JSON.stringify(val) : null)) + .refine((val) => !val || val.length <= 1000, { + message: "Metadata must be less than 1000 characters when stringified", + }) + .describe( + "Additional metadata to be stored with the lead event. Max 1000 characters.", + ), mode: z .enum(["async", "wait"]) .default("async") diff --git a/apps/web/lib/zod/schemas/sales.ts b/apps/web/lib/zod/schemas/sales.ts index 49e34962d3c..9de60c2f4c2 100644 --- a/apps/web/lib/zod/schemas/sales.ts +++ b/apps/web/lib/zod/schemas/sales.ts @@ -56,7 +56,13 @@ export const trackSaleRequestSchema = z.object({ .record(z.unknown()) .nullish() .default(null) - .describe("Additional metadata to be stored with the sale event."), + .transform((val) => (val ? JSON.stringify(val) : null)) + .refine((val) => !val || val.length <= 1000, { + message: "Metadata must be less than 1000 characters when stringified", + }) + .describe( + "Additional metadata to be stored with the sale event. Max 1000 characters.", + ), leadEventName: z .string() .nullish() From 6c9d1f8cf78c19241b2cc0528545f86b017c8ec7 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 18 Apr 2025 11:46:08 -0700 Subject: [PATCH 4/6] reduce maxlength to 500 --- apps/web/lib/zod/schemas/leads.ts | 4 ++-- apps/web/lib/zod/schemas/sales.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/lib/zod/schemas/leads.ts b/apps/web/lib/zod/schemas/leads.ts index 86ad7367132..ab0c53f3d17 100644 --- a/apps/web/lib/zod/schemas/leads.ts +++ b/apps/web/lib/zod/schemas/leads.ts @@ -66,8 +66,8 @@ export const trackLeadRequestSchema = z.object({ .nullish() .default(null) .transform((val) => (val ? JSON.stringify(val) : null)) - .refine((val) => !val || val.length <= 1000, { - message: "Metadata must be less than 1000 characters when stringified", + .refine((val) => !val || val.length <= 500, { + message: "Metadata must be less than 500 characters when stringified", }) .describe( "Additional metadata to be stored with the lead event. Max 1000 characters.", diff --git a/apps/web/lib/zod/schemas/sales.ts b/apps/web/lib/zod/schemas/sales.ts index 9de60c2f4c2..375000b8181 100644 --- a/apps/web/lib/zod/schemas/sales.ts +++ b/apps/web/lib/zod/schemas/sales.ts @@ -57,11 +57,11 @@ export const trackSaleRequestSchema = z.object({ .nullish() .default(null) .transform((val) => (val ? JSON.stringify(val) : null)) - .refine((val) => !val || val.length <= 1000, { - message: "Metadata must be less than 1000 characters when stringified", + .refine((val) => !val || val.length <= 500, { + message: "Metadata must be less than 500 characters when stringified", }) .describe( - "Additional metadata to be stored with the sale event. Max 1000 characters.", + "Additional metadata to be stored with the sale event. Max 500 characters.", ), leadEventName: z .string() From 73bee20f0aa5bae7357b0e49f87fb9321d40a003 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 18 Apr 2025 11:53:25 -0700 Subject: [PATCH 5/6] increase max length to 10K --- apps/web/lib/zod/schemas/leads.ts | 6 +++--- apps/web/lib/zod/schemas/sales.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/lib/zod/schemas/leads.ts b/apps/web/lib/zod/schemas/leads.ts index ab0c53f3d17..56f4cd95928 100644 --- a/apps/web/lib/zod/schemas/leads.ts +++ b/apps/web/lib/zod/schemas/leads.ts @@ -66,11 +66,11 @@ export const trackLeadRequestSchema = z.object({ .nullish() .default(null) .transform((val) => (val ? JSON.stringify(val) : null)) - .refine((val) => !val || val.length <= 500, { - message: "Metadata must be less than 500 characters when stringified", + .refine((val) => !val || val.length <= 10000, { + message: "Metadata must be less than 10000 characters when stringified", }) .describe( - "Additional metadata to be stored with the lead event. Max 1000 characters.", + "Additional metadata to be stored with the lead event. Max 10,000 characters.", ), mode: z .enum(["async", "wait"]) diff --git a/apps/web/lib/zod/schemas/sales.ts b/apps/web/lib/zod/schemas/sales.ts index 375000b8181..a5a9867721d 100644 --- a/apps/web/lib/zod/schemas/sales.ts +++ b/apps/web/lib/zod/schemas/sales.ts @@ -57,11 +57,11 @@ export const trackSaleRequestSchema = z.object({ .nullish() .default(null) .transform((val) => (val ? JSON.stringify(val) : null)) - .refine((val) => !val || val.length <= 500, { - message: "Metadata must be less than 500 characters when stringified", + .refine((val) => !val || val.length <= 10000, { + message: "Metadata must be less than 10000 characters when stringified", }) .describe( - "Additional metadata to be stored with the sale event. Max 500 characters.", + "Additional metadata to be stored with the sale event. Max 10,000 characters.", ), leadEventName: z .string() From 2c54493b5763fef1ce7c0056f063314619bf2729 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 18 Apr 2025 11:55:35 -0700 Subject: [PATCH 6/6] combine to refine only --- apps/web/lib/zod/schemas/leads.ts | 5 ++--- apps/web/lib/zod/schemas/sales.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/web/lib/zod/schemas/leads.ts b/apps/web/lib/zod/schemas/leads.ts index 56f4cd95928..bd0bfa86ed9 100644 --- a/apps/web/lib/zod/schemas/leads.ts +++ b/apps/web/lib/zod/schemas/leads.ts @@ -65,9 +65,8 @@ export const trackLeadRequestSchema = z.object({ .record(z.unknown()) .nullish() .default(null) - .transform((val) => (val ? JSON.stringify(val) : null)) - .refine((val) => !val || val.length <= 10000, { - message: "Metadata must be less than 10000 characters when stringified", + .refine((val) => !val || JSON.stringify(val).length <= 10000, { + message: "Metadata must be less than 10,000 characters when stringified", }) .describe( "Additional metadata to be stored with the lead event. Max 10,000 characters.", diff --git a/apps/web/lib/zod/schemas/sales.ts b/apps/web/lib/zod/schemas/sales.ts index a5a9867721d..74af54369ce 100644 --- a/apps/web/lib/zod/schemas/sales.ts +++ b/apps/web/lib/zod/schemas/sales.ts @@ -56,9 +56,8 @@ export const trackSaleRequestSchema = z.object({ .record(z.unknown()) .nullish() .default(null) - .transform((val) => (val ? JSON.stringify(val) : null)) - .refine((val) => !val || val.length <= 10000, { - message: "Metadata must be less than 10000 characters when stringified", + .refine((val) => !val || JSON.stringify(val).length <= 10000, { + message: "Metadata must be less than 10,000 characters when stringified", }) .describe( "Additional metadata to be stored with the sale event. Max 10,000 characters.",