Files
Benny JooGitHubbenny@cal.com <sldisek783@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

177 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import process from "node:process";
import { RedisService } from "@calcom/features/redis/RedisService";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { RatelimitResponse } from "@unkey/ratelimit";
import { hashAPIKey } from "./apiKeys";
// This is the number of times a user can exceed the rate limit before being locked
const DEFAULT_AUTOLOCK_THRESHOLD = 5;
// 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;
identifierType: "email" | "userId" | "SMS" | "apiKey";
rateLimitResponse: RatelimitResponse;
identifierKeyword?: string; // For instances where we have like "addSecondaryEmail.${email}"
autolockThreshold?: number;
autolockDuration?: number; // in milliseconds
}
export enum LockReason {
RATE_LIMIT = "Auto-locking user due to rate limit exceeded",
}
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,
rateLimitResponse,
identifierKeyword,
autolockThreshold = DEFAULT_AUTOLOCK_THRESHOLD,
autolockDuration = DEFAULT_CHECK_THRESHOLD_WINDOW_DURATION,
}: HandleAutoLockInput): Promise<boolean> {
const { success, remaining } = rateLimitResponse;
const UPSTASH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_TOKEN && process.env.UPSTASH_REDIS_REST_URL;
if (!UPSTASH_ENV_FOUND) {
log.warn("Skipping auto lock because UPSTASH env variables are not set");
return false;
}
const identifier = identifierKeyword
? _identifier.toString().replace(`${identifierKeyword}.`, "")
: _identifier;
if (!success && remaining <= 0) {
const redis = new RedisService();
const lockKey = `autolock:${identifierType}${
identifierKeyword ? `:${identifierKeyword}` : ""
}:${identifier}.count`;
try {
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, LockReason.RATE_LIMIT);
await redis.del(lockKey);
return true;
}
await redis.set(lockKey, (currentCount + 1).toString());
await redis.expire(lockKey, Math.floor(autolockDuration / 1000));
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;
}
}
return false;
}
export async function lockUser(identifierType: string, identifier: string, lockReason: LockReason) {
if (!identifier) {
return;
}
type UserType = {
id: number;
email: string;
username: string | null;
} | null;
let user: UserType = null;
switch (identifierType) {
case "userId":
user = await prisma.user.update({
where: { id: Number(identifier) },
data: { locked: true },
select: {
id: true,
email: true,
username: true,
},
});
break;
case "email":
user = await prisma.user.update({
where: { email: identifier },
data: { locked: true },
select: {
id: true,
email: true,
username: true,
},
});
break;
case "apiKey": {
const hashedApiKey = hashAPIKey(identifier);
const apiKey = await prisma.apiKey.findUnique({
where: { hashedKey: hashedApiKey },
include: {
user: {
select: {
id: true,
email: true,
username: true,
},
},
},
});
if (!apiKey?.user) {
throw new Error("No user found for this API key.");
}
user = await prisma.user.update({
where: { id: apiKey.user.id },
data: { locked: true },
select: {
id: true,
email: true,
username: true,
},
});
break;
}
// Leaving SMS here but it is handled differently via checkRateLimitForSMS that auto locks
case "SMS":
break;
default:
throw new Error("Invalid identifier type for locking");
}
if (user && process.env.NEXT_PUBLIC_SENTRY_DSN) {
log.warn(lockReason, {
userId: user.id,
email: user.email,
username: user.username,
});
}
}