From d9ebf26e21d4fa8584010815e096e73f779f8f11 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Wed, 10 Dec 2025 01:04:21 +0000 Subject: [PATCH] chore: standarize rate limit structure (#25736) * feat: add toggle to opt out of booking title translation in instant meetings (#25547) * feat: add toggle to opt out of booking title translation in instant meetings Co-Authored-By: joe@cal.com * fix: add autoTranslateTitleEnabled to test builder Co-Authored-By: joe@cal.com * fix: respect autoTranslateTitleEnabled toggle in InstantBookingCreateService Co-Authored-By: joe@cal.com * fix: fetch autoTranslateTitleEnabled directly in InstantBookingCreateService Co-Authored-By: joe@cal.com * refactor: rename autoTranslateTitleEnabled to autoTranslateInstantMeetingTitleEnabled - Renamed field to clarify it only applies to instant meeting title translation - Moved Prisma query to EventTypeRepository.findInstantMeetingConfigById() - Removed title translation logic from update.handler.ts (flag only controls instant meeting title) - Updated all references across the codebase - Added new i18n translation keys for the renamed field Co-Authored-By: joe@cal.com * fix: add autoTranslateInstantMeetingTitleEnabled to test destructuring patterns Co-Authored-By: joe@cal.com * fix: use Prisma.TeamCreateInput type for metadata in test helper Co-Authored-By: joe@cal.com * fix: add autoTranslateInstantMeetingTitleEnabled to mockUpdatedEventType in test Co-Authored-By: joe@cal.com * refactor: use generic findByIdMinimal instead of business-specific method - Add autoTranslateInstantMeetingTitleEnabled to eventTypeSelect constant - Remove findInstantMeetingConfigById from EventTypeRepository - Update InstantBookingCreateService to use findByIdMinimal Co-Authored-By: joe@cal.com * refactor: remove autoTranslateInstantMeetingTitleEnabled from eventTypeSelect (not needed for findByIdMinimal) Co-Authored-By: joe@cal.com * feat: add autoTranslateInstantMeetingTitleEnabled to event type form defaults - Rename shouldTranslateTitle to shouldAutoTranslateInstantMeetingTitle for clarity - Add autoTranslateInstantMeetingTitleEnabled to findById and findByIdForOrgAdmin selects - Add autoTranslateInstantMeetingTitleEnabled to useEventTypeForm defaultValues Co-Authored-By: joe@cal.com * fix: only update autoTranslateInstantMeetingTitleEnabled when explicitly provided Fixes issue where omitting the field in update requests would overwrite the saved opt-out setting with the default value (true). Now the field is only included in the update payload when explicitly provided. Co-Authored-By: joe@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: Create standard for rate limits --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/app/api/auth/forgot-password/route.ts | 2 +- apps/web/pages/api/book/event.ts | 2 +- apps/web/pages/api/book/recurring-event.ts | 2 +- packages/features/auth/lib/verifyEmail.ts | 2 +- .../calAIPhone/providers/retellAI/RetellAIService.test.ts | 2 +- .../calAIPhone/providers/retellAI/services/CallService.ts | 4 ++-- .../retellAI/services/__tests__/CallService.test.ts | 4 ++-- .../ee/workflows/lib/reminders/aiPhoneCallManager.ts | 2 +- packages/features/tasker/tasks/executeAIPhoneCall.ts | 4 ++-- packages/sms/sms-manager.ts | 6 +++--- packages/sms/test/sms-manager.test.ts | 2 +- .../routers/loggedInViewer/addSecondaryEmail.handler.ts | 2 +- .../server/routers/viewer/auth/resendVerifyEmail.handler.ts | 2 +- .../routers/viewer/auth/sendVerifyEmailCode.handler.ts | 2 +- .../routers/viewer/organizations/verifyCode.handler.ts | 2 +- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/web/app/api/auth/forgot-password/route.ts b/apps/web/app/api/auth/forgot-password/route.ts index 9611abdf4b..87c7abcad1 100644 --- a/apps/web/app/api/auth/forgot-password/route.ts +++ b/apps/web/app/api/auth/forgot-password/route.ts @@ -29,7 +29,7 @@ async function handler(req: NextRequest) { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: piiHasher.hash(ip), + identifier: `forgotPassword:${piiHasher.hash(ip)}`, }); try { diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 3782520a94..63b6e679e7 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -36,7 +36,7 @@ async function handler(req: NextApiRequest & { userId?: number; traceContext: Tr await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: piiHasher.hash(userIp), + identifier: `createBooking:${piiHasher.hash(userIp)}`, }); const session = await getServerSession({ req }); diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index 23b9a28fae..0fdd9d3cae 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -38,7 +38,7 @@ async function handler(req: NextApiRequest & RequestMeta) { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: piiHasher.hash(userIp), + identifier: `createRecurringBooking:${piiHasher.hash(userIp)}`, }); const session = await getServerSession({ req }); /* To mimic API behavior and comply with types */ diff --git a/packages/features/auth/lib/verifyEmail.ts b/packages/features/auth/lib/verifyEmail.ts index 48a3c0da09..7c14a77101 100644 --- a/packages/features/auth/lib/verifyEmail.ts +++ b/packages/features/auth/lib/verifyEmail.ts @@ -56,7 +56,7 @@ export const sendEmailVerification = async ({ await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: hashEmail(email), + identifier: `sendEmailVerification:${hashEmail(email)}`, }); await prisma.verificationToken.create({ diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts index 3a52833e8e..5a8e4e29f8 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts @@ -917,7 +917,7 @@ describe("RetellAIService", () => { expect(vi.mocked(checkRateLimitAndThrowError)).toHaveBeenCalledWith({ rateLimitingType: "core", - identifier: "test-call:1", + identifier: "createTestCall:1", }); expect(result).toEqual({ callId: "call-123", diff --git a/packages/features/calAIPhone/providers/retellAI/services/CallService.ts b/packages/features/calAIPhone/providers/retellAI/services/CallService.ts index ce363adae4..d05930aecf 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/CallService.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/CallService.ts @@ -97,7 +97,7 @@ export class CallService { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `test-call:${userId}`, + identifier: `createTestCall:${userId}`, }); const toNumber = phoneNumber?.trim(); @@ -192,7 +192,7 @@ export class CallService { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `web-call:${userId}`, + identifier: `createWebCall:${userId}`, }); const agent = await this.deps.agentRepository.findByIdWithCallAccess({ diff --git a/packages/features/calAIPhone/providers/retellAI/services/__tests__/CallService.test.ts b/packages/features/calAIPhone/providers/retellAI/services/__tests__/CallService.test.ts index b8ed0e70ea..70f6173512 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/__tests__/CallService.test.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/__tests__/CallService.test.ts @@ -207,7 +207,7 @@ describe("CallService", () => { expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({ rateLimitingType: "core", - identifier: "test-call:1", + identifier: "createTestCall:1", }); }); @@ -224,7 +224,7 @@ describe("CallService", () => { expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({ rateLimitingType: "core", - identifier: "test-call:42", + identifier: "createTestCall:42", }); }); diff --git a/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts b/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts index 7ed283e581..4f6bf5a4e1 100644 --- a/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts +++ b/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts @@ -400,7 +400,7 @@ const scheduleAIPhoneCallTask = async (args: ScheduleAIPhoneCallTaskArgs) => { if (userId) { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `ai-phone-call:${userId}`, + identifier: `executeAIPhoneCall:${userId}`, }); } diff --git a/packages/features/tasker/tasks/executeAIPhoneCall.ts b/packages/features/tasker/tasks/executeAIPhoneCall.ts index af8a270dd2..116cad43f7 100644 --- a/packages/features/tasker/tasks/executeAIPhoneCall.ts +++ b/packages/features/tasker/tasks/executeAIPhoneCall.ts @@ -174,9 +174,9 @@ export async function executeAIPhoneCall(payload: string) { } const rateLimitIdentifier = data.teamId - ? `ai-phone-call:team:${data.teamId}` + ? `executeAIPhoneCall:team-${data.teamId}` : data.userId - ? `ai-phone-call:user:${data.userId}` + ? `executeAIPhoneCall:user-${data.userId}` : null; if (!rateLimitIdentifier) { diff --git a/packages/sms/sms-manager.ts b/packages/sms/sms-manager.ts index aafe6a5a42..aea61317a9 100644 --- a/packages/sms/sms-manager.ts +++ b/packages/sms/sms-manager.ts @@ -31,10 +31,10 @@ const handleSendingSMS = async ({ // If neither is provided(Just in case), we check the rate limit for the reminderPhone. await checkSMSRateLimit({ identifier: teamId - ? `handleSendingSMS:team:${teamId}` + ? `handleSendingSMS:team-${teamId}` : organizerUserId - ? `handleSendingSMS:user:${organizerUserId}` - : `handleSendingSMS:user:${piiHasher.hash(reminderPhone)}`, + ? `handleSendingSMS:org-user-${organizerUserId}` + : `handleSendingSMS:user-${piiHasher.hash(reminderPhone)}`, rateLimitingType: "sms", }); diff --git a/packages/sms/test/sms-manager.test.ts b/packages/sms/test/sms-manager.test.ts index b787feaba8..a7e1aec212 100644 --- a/packages/sms/test/sms-manager.test.ts +++ b/packages/sms/test/sms-manager.test.ts @@ -141,7 +141,7 @@ describe("SMSManager", () => { const result = await smsManager.sendSMSToAttendee(mockCalEvent.attendees[0], "test-booking-uid"); expect(checkSMSRateLimit).toHaveBeenCalledWith({ - identifier: "handleSendingSMS:user:1", + identifier: "handleSendingSMS:org-user-1", rateLimitingType: "sms", }); diff --git a/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts b/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts index d6ffc95935..7af35b9460 100644 --- a/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/addSecondaryEmail.handler.ts @@ -22,7 +22,7 @@ export const addSecondaryEmailHandler = async ({ ctx, input }: AddSecondaryEmail await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `addSecondaryEmail.${user.id}`, + identifier: `addSecondaryEmail:${user.id}`, }); const existingPrimaryEmail = await prisma.user.findUnique({ diff --git a/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts b/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts index 81d05c8f9d..86b2225cc0 100644 --- a/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/resendVerifyEmail.handler.ts @@ -24,7 +24,7 @@ export const resendVerifyEmail = async ({ input, ctx }: ResendEmailOptions) => { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `resendVerifyEmail.${ctx.user.id}`, + identifier: `resendVerifyEmail:${ctx.user.id}`, }); let emailVerified = Boolean(ctx.user.emailVerified); diff --git a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts index 3dd9627742..b7d0bd7637 100644 --- a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts @@ -27,7 +27,7 @@ export const sendVerifyEmailCode = async ({ }) => { await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: `emailVerifyByCode.${identifier}`, + identifier: `sendVerifyEmailCode:${identifier}`, }); return await sendEmailVerificationByCode({ diff --git a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts index 4cccb88f88..b960ab453e 100644 --- a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts @@ -48,7 +48,7 @@ export const verifyCode = async ({ await checkRateLimitAndThrowError({ rateLimitingType: "core", - identifier: hashEmail(email), + identifier: `verifyCode:${hashEmail(email)}`, }); const secret = createHash("md5")