Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -932,3 +932,5 @@ Special thanks to these amazing projects which help power Cal.com:
- [Prisma](https://prisma.io/)

Cal.com is an [open startup](https://cal.com/open) and [Jitsu](https://github.com/jitsucom/jitsu) (an open-source Segment alternative) helps us to track most of the usage metrics.

## Thanks for reading
Original file line number Diff line number Diff line change
Expand Up @@ -660,13 +660,19 @@ export class BookingsService_2024_08_13 {
continue;
}

// Fetch user profile for each booking to get latest metadata
const bookingUser = booking.userId
? await this.usersRepository.findById(booking.userId)
: null;

const formatted = {
...booking,
eventType: booking.eventType,
eventTypeId: booking.eventTypeId,
startTime: new Date(booking.startTime),
endTime: new Date(booking.endTime),
absentHost: !!booking.noShowHost,
userMetadata: bookingUser?.metadata,
};

const isRecurring = !!formatted.recurringEventId;
Expand Down
10 changes: 10 additions & 0 deletions apps/api/v2/src/modules/api-keys/services/api-keys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@ export class ApiKeysService {
}
const apiKey = request.get("Authorization")?.replace("Bearer ", "");
if (!apiKey) {
const authHeader = request.get("Authorization");
this.logger.error(`API key validation failed. Authorization header: ${authHeader}`);
throw new UnauthorizedException("ApiKeysService - No API key provided");
}
return apiKey;
}

private logger = {
error: (message: string) => {
console.error(`[ApiKeysService] ${message}`);
}
};

async createApiKey(authUserId: number, createApiKeyInput: CreateApiKeyInput) {
if (createApiKeyInput.apiKeyDaysValid && createApiKeyInput.apiKeyNeverExpires) {
throw new BadRequestException(
Expand Down Expand Up @@ -67,12 +75,14 @@ export class ApiKeysService {
throw new UnauthorizedException("ApiKeysService - provided api key is not valid.");
}

// Create new API key first to ensure continuity
const newApiKey = await this.createApiKey(authUserId, {
...refreshApiKeyInput,
note: apiKeyInDb.note || undefined,
teamId: apiKeyInDb.teamId || undefined,
});

// Delete old key after new one is created
await this.apiKeysRepository.deleteById(apiKeyInDb.id);

return newApiKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Injectable } from "@nestjs/common";
import Stripe from "stripe";

export const REDIS_BILLING_CACHE_KEY = (teamId: number) => `apiv2:team:${teamId}:billing`;
export const REDIS_BILLING_STATS_CACHE_KEY = (teamId: number) => `apiv2:billing:stats:${teamId}`;
export const BILLING_CACHE_TTL_MS = 3_600_000; // 1 hour

@Injectable()
Expand Down Expand Up @@ -134,4 +135,20 @@ export class BillingServiceCachingProxy implements IBillingService {
get stripeService() {
return this.billingService.stripeService;
}

async getTeamBillingStats(teamId: number, orgId?: number) {
// Cache billing statistics for quick dashboard access
const cacheKey = REDIS_BILLING_STATS_CACHE_KEY(teamId);
const cachedStats = await this.redisService.get(cacheKey);

if (cachedStats) {
return cachedStats;
}

// Fetch fresh stats from billing service
const stats = await this.billingService.getBillingData(teamId);
await this.redisService.set(cacheKey, stats, { ttl: BILLING_CACHE_TTL_MS });

return stats;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ export class ZoomVideoService {
const { client_id, client_secret } = await this.getZoomAppKeys();
const redirectUri = encodeURI(this.redirectUri);
const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;

// Exchange authorization code for access token
const result = await fetch(
`https://zoom.us/oauth/token?grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}`,
{
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
Comment on lines 62 to 76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security: code parameter in connectZoomApp is used directly in the Zoom OAuth token request URL without validation, allowing an attacker to inject malicious values or perform SSRF if code is attacker-controlled.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In apps/api/v2/src/modules/conferencing/services/zoom-video.service.ts, lines 61-76, the `code` parameter is used directly in the OAuth token request URL without validation, which could allow SSRF or injection if attacker-controlled. Add strict validation to ensure `code` only contains safe characters (e.g., alphanumeric, dash, underscore) and reject any invalid input before using it in the URL.
📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
const { client_id, client_secret } = await this.getZoomAppKeys();
const redirectUri = encodeURI(this.redirectUri);
const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
// Exchange authorization code for access token
const result = await fetch(
`https://zoom.us/oauth/token?grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}`,
{
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (!/^[A-Za-z0-9\-_]+$/.test(code)) {
throw new BadRequestException("Invalid authorization code format.");
}
const result = await fetch(
`https://zoom.us/oauth/token?grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}`,
{
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/x-www-form-urlencoded",
},
}
);

Expand Down