diff --git a/packages/lib/__tests__/autoLock.test.ts b/packages/lib/__tests__/autoLock.test.ts index 3a103142de..b92847a84d 100644 --- a/packages/lib/__tests__/autoLock.test.ts +++ b/packages/lib/__tests__/autoLock.test.ts @@ -22,12 +22,6 @@ vi.mock("@calcom/prisma", () => ({ }, }, })); -vi.mock("@sentry/nextjs", () => ({ - captureException: vi.fn(), - captureMessage: vi.fn(), - setUser: vi.fn(), - setTag: vi.fn(), -})); describe("autoLock", () => { const mockRedis = { @@ -45,13 +39,11 @@ describe("autoLock", () => { // Mock environment variables process.env.UPSTASH_REDIS_REST_TOKEN = "test-token"; process.env.UPSTASH_REDIS_REST_URL = "test-url"; - process.env.NEXT_PUBLIC_SENTRY_DSN = "sentry-dsn"; }); afterEach(() => { delete process.env.UPSTASH_REDIS_REST_TOKEN; delete process.env.UPSTASH_REDIS_REST_URL; - delete process.env.NEXT_PUBLIC_SENTRY_DSN; }); describe("handleAutoLock", () => { @@ -110,7 +102,7 @@ describe("autoLock", () => { }); expect(mockRedis.set).toHaveBeenCalledWith("autolock:email:test@example.com.count", "3"); - expect(mockRedis.expire).toHaveBeenCalledWith("autolock:email:test@example.com.count", 3600); + expect(mockRedis.expire).toHaveBeenCalledWith("autolock:email:test@example.com.count", 1800); expect(prisma.user.update).not.toHaveBeenCalled(); }); @@ -262,7 +254,7 @@ describe("autoLock", () => { }); expect(mockRedis.set).toHaveBeenCalledWith("autolock:email:test@example.com.count", "1"); - expect(mockRedis.expire).toHaveBeenCalledWith("autolock:email:test@example.com.count", 3600); + expect(mockRedis.expire).toHaveBeenCalledWith("autolock:email:test@example.com.count", 1800); }); it("should handle Redis errors gracefully", async () => { diff --git a/packages/lib/autoLock.ts b/packages/lib/autoLock.ts index ae6320eb5c..dd6c878343 100644 --- a/packages/lib/autoLock.ts +++ b/packages/lib/autoLock.ts @@ -1,12 +1,15 @@ -import * as Sentry from "@sentry/nextjs"; import type { RatelimitResponse } from "@unkey/ratelimit"; import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys"; import { RedisService } from "@calcom/features/redis/RedisService"; import prisma from "@calcom/prisma"; +import logger from "./logger"; + +// This is the number of times a user can exceed the rate limit before being locked const DEFAULT_AUTOLOCK_THRESHOLD = 5; -const DEFAULT_CHECK_THRESHOLD_WINDOW_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds +// This is the duration of the rate limit check window +const DEFAULT_CHECK_THRESHOLD_WINDOW_DURATION = 60 * 30 * 1000; // 30 minutes in milliseconds interface HandleAutoLockInput { identifier: string; @@ -17,6 +20,13 @@ interface HandleAutoLockInput { autolockDuration?: number; // in milliseconds } +const log = logger.getSubLogger({ prefix: ["[autoLock]"] }); + +/** + * The "Requests to Hit Limit × Threshold" shows how many requests would be needed to trigger an auto-lock if a user consistently hits + * its their rate limit. For example, in the "core" * namespace, a user would need to make at least 50 requests (10 limit × 5 threshold) + * within the 30-minute window to get auto-locked. + */ export async function handleAutoLock({ identifier: _identifier, identifierType, @@ -30,7 +40,7 @@ export async function handleAutoLock({ const UPSTASH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_TOKEN && process.env.UPSTASH_REDIS_REST_URL; if (!UPSTASH_ENV_FOUND) { - console.log("Skipping auto lock because UPSTASH env variables are not set"); + log.warn("Skipping auto lock because UPSTASH env variables are not set"); return false; } @@ -48,8 +58,17 @@ export async function handleAutoLock({ const count = await redis.get(lockKey); const currentCount = count ? parseInt(count.toString(), 10) : 0; + log.info( + `Rate limit exceeded for ${identifierType}: ${identifier}. Current count: ${currentCount}/${autolockThreshold}` + ); + // If they have exceeded the threshold, lock them if (currentCount + 1 >= autolockThreshold) { + log.warn( + `Auto-locking ${identifierType}: ${identifier}. Threshold reached: ${ + currentCount + 1 + }/${autolockThreshold}` + ); await lockUser(identifierType, identifier); await redis.del(lockKey); return true; @@ -60,8 +79,10 @@ export async function handleAutoLock({ return false; } catch (err) { if (err instanceof Error && err.message === "No user found for this API key.") { + log.error(`Error in auto-lock: No user found for API key: ${identifier}`); throw err; } + log.error(`Error in auto-lock process: ${err instanceof Error ? err.message : String(err)}`); return false; } } @@ -142,13 +163,10 @@ async function lockUser(identifierType: string, identifier: string) { } if (user && process.env.NEXT_PUBLIC_SENTRY_DSN) { - Sentry.setUser({ - id: user.id.toString(), + log.warn("Auto-locking user due to rate limit exceeded", { + userId: user.id, email: user.email, - username: user.username ?? undefined, + username: user.username, }); - Sentry.setTag("admin_notify", true); - Sentry.setTag("auto_lock", true); - Sentry.captureMessage(`User ${user.email} has been locked due to suspicious activity.`, "warning"); } }