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 <j.auyeung419@gmail.com>

* fix: add autoTranslateTitleEnabled to test builder

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: respect autoTranslateTitleEnabled toggle in InstantBookingCreateService

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: fetch autoTranslateTitleEnabled directly in InstantBookingCreateService

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.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 <j.auyeung419@gmail.com>

* fix: add autoTranslateInstantMeetingTitleEnabled to test destructuring patterns

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use Prisma.TeamCreateInput type for metadata in test helper

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add autoTranslateInstantMeetingTitleEnabled to mockUpdatedEventType in test

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.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 <j.auyeung419@gmail.com>

* refactor: remove autoTranslateInstantMeetingTitleEnabled from eventTypeSelect (not needed for findByIdMinimal)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.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 <j.auyeung419@gmail.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 <j.auyeung419@gmail.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>
This commit is contained in:
Alex van Andel
2025-12-10 01:04:21 +00:00
committed by GitHub
co-authored by joe@cal.com Devin AI Joe Au-Yeung
parent cb13874574
commit d9ebf26e21
15 changed files with 20 additions and 20 deletions
@@ -29,7 +29,7 @@ async function handler(req: NextRequest) {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: piiHasher.hash(ip),
identifier: `forgotPassword:${piiHasher.hash(ip)}`,
});
try {
+1 -1
View File
@@ -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 });
+1 -1
View File
@@ -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 */
+1 -1
View File
@@ -56,7 +56,7 @@ export const sendEmailVerification = async ({
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: hashEmail(email),
identifier: `sendEmailVerification:${hashEmail(email)}`,
});
await prisma.verificationToken.create({
@@ -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",
@@ -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({
@@ -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",
});
});
@@ -400,7 +400,7 @@ const scheduleAIPhoneCallTask = async (args: ScheduleAIPhoneCallTaskArgs) => {
if (userId) {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `ai-phone-call:${userId}`,
identifier: `executeAIPhoneCall:${userId}`,
});
}
@@ -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) {
+3 -3
View File
@@ -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",
});
+1 -1
View File
@@ -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",
});
@@ -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({
@@ -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);
@@ -27,7 +27,7 @@ export const sendVerifyEmailCode = async ({
}) => {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `emailVerifyByCode.${identifier}`,
identifier: `sendVerifyEmailCode:${identifier}`,
});
return await sendEmailVerificationByCode({
@@ -48,7 +48,7 @@ export const verifyCode = async ({
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: hashEmail(email),
identifier: `verifyCode:${hashEmail(email)}`,
});
const secret = createHash("md5")