fix: add improved logging and decrease ttl (#19979)

* add improved logging and decrease ttl

* fix test TTL time

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
This commit is contained in:
sean-brydon
2025-03-12 10:26:07 +00:00
committed by GitHub
co-authored by Tushar Bhatt
parent b644ec58af
commit 49cb86dfe9
2 changed files with 29 additions and 19 deletions
+2 -10
View File
@@ -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 () => {
+27 -9
View File
@@ -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");
}
}