Skip to content

Commit 7c5c8b0

Browse files
Merge branch 'main' into zomars/cal-3378-allow-to-select-hungarian-language
2 parents e7cca0e + 9a473d5 commit 7c5c8b0

File tree

332 files changed

+9832
-2410
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

332 files changed

+9832
-2410
lines changed

.github/workflows/semantic-pull-requests.yml

-9
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,3 @@ jobs:
3737
```
3838
${{ steps.lint_pr_title.outputs.error_message }}
3939
```
40-
41-
# Delete a previous comment when the issue has been resolved
42-
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
43-
uses: marocchino/sticky-pull-request-comment@v2
44-
with:
45-
header: pr-title-lint-error
46-
message: |
47-
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
48-

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ To develop locally:
9999

100100
- Duplicate `.env.example` to `.env`.
101101
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
102-
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
102+
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
103103

104104
6. Setup Node
105105
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:

apps/api/v1/instrumentation.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as Sentry from "@sentry/nextjs";
2+
3+
export function register() {
4+
if (process.env.NEXT_RUNTIME === "nodejs") {
5+
Sentry.init({
6+
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
7+
// reduce sample rate to 10% on production
8+
tracesSampleRate: process.env.NODE_ENV !== "production" ? 1.0 : 0.1,
9+
});
10+
}
11+
12+
if (process.env.NEXT_RUNTIME === "edge") {
13+
Sentry.init({
14+
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
15+
});
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { Request, Response } from "express";
2+
import type { NextApiResponse, NextApiRequest } from "next";
3+
import { createMocks } from "node-mocks-http";
4+
import { describe, it, expect, vi } from "vitest";
5+
6+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
7+
8+
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
9+
10+
type CustomNextApiRequest = NextApiRequest & Request;
11+
type CustomNextApiResponse = NextApiResponse & Response;
12+
13+
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
14+
checkRateLimitAndThrowError: vi.fn(),
15+
}));
16+
17+
describe("rateLimitApiKey middleware", () => {
18+
it("should return 401 if no apiKey is provided", async () => {
19+
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
20+
method: "GET",
21+
query: {},
22+
});
23+
24+
await rateLimitApiKey(req, res, vi.fn() as any);
25+
26+
expect(res._getStatusCode()).toBe(401);
27+
expect(res._getJSONData()).toEqual({ message: "No apiKey provided" });
28+
});
29+
30+
it("should call checkRateLimitAndThrowError with correct parameters", async () => {
31+
const { req, res } = createMocks({
32+
method: "GET",
33+
query: { apiKey: "test-key" },
34+
});
35+
36+
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
37+
limit: 100,
38+
remaining: 99,
39+
reset: Date.now(),
40+
});
41+
42+
// @ts-expect-error weird typing between middleware and createMocks
43+
await rateLimitApiKey(req, res, vi.fn() as any);
44+
45+
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
46+
identifier: "test-key",
47+
rateLimitingType: "api",
48+
onRateLimiterResponse: expect.any(Function),
49+
});
50+
});
51+
52+
it("should set rate limit headers correctly", async () => {
53+
const { req, res } = createMocks({
54+
method: "GET",
55+
query: { apiKey: "test-key" },
56+
});
57+
58+
const rateLimiterResponse = {
59+
limit: 100,
60+
remaining: 99,
61+
reset: Date.now(),
62+
};
63+
64+
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
65+
onRateLimiterResponse(rateLimiterResponse);
66+
});
67+
68+
// @ts-expect-error weird typing between middleware and createMocks
69+
await rateLimitApiKey(req, res, vi.fn() as any);
70+
71+
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
72+
expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining);
73+
expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset);
74+
});
75+
76+
it("should return 429 if rate limit is exceeded", async () => {
77+
const { req, res } = createMocks({
78+
method: "GET",
79+
query: { apiKey: "test-key" },
80+
});
81+
82+
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
83+
84+
// @ts-expect-error weird typing between middleware and createMocks
85+
await rateLimitApiKey(req, res, vi.fn() as any);
86+
87+
expect(res._getStatusCode()).toBe(429);
88+
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
89+
});
90+
});

apps/api/v1/lib/helpers/rateLimitApiKey.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
66
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
77

88
// TODO: Add a way to add trusted api keys
9-
await checkRateLimitAndThrowError({
10-
identifier: req.query.apiKey as string,
11-
rateLimitingType: "api",
12-
onRateLimiterResponse: (response) => {
13-
res.setHeader("X-RateLimit-Limit", response.limit);
14-
res.setHeader("X-RateLimit-Remaining", response.remaining);
15-
res.setHeader("X-RateLimit-Reset", response.reset);
16-
},
17-
});
9+
try {
10+
await checkRateLimitAndThrowError({
11+
identifier: req.query.apiKey as string,
12+
rateLimitingType: "api",
13+
onRateLimiterResponse: (response) => {
14+
res.setHeader("X-RateLimit-Limit", response.limit);
15+
res.setHeader("X-RateLimit-Remaining", response.remaining);
16+
res.setHeader("X-RateLimit-Reset", response.reset);
17+
},
18+
});
19+
} catch (error) {
20+
res.status(429).json({ message: "Rate limit exceeded" });
21+
}
1822

1923
await next();
2024
};

apps/api/v1/lib/validations/shared/queryIdTransformParseInt.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ export const withValidQueryIdTransformParseInt = withValidation({
1414
type: "Zod",
1515
mode: "query",
1616
});
17+
18+
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
19+
recordingId: z.string(),
20+
});

apps/api/v1/next.config.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ const { withAxiom } = require("next-axiom");
22
const { withSentryConfig } = require("@sentry/nextjs");
33

44
const plugins = [withAxiom];
5+
6+
/** @type {import("next").NextConfig} */
57
const nextConfig = {
8+
experimental: {
9+
instrumentationHook: true,
10+
},
611
transpilePackages: [
712
"@calcom/app-store",
813
"@calcom/core",
@@ -87,12 +92,12 @@ const nextConfig = {
8792
};
8893

8994
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
90-
nextConfig["sentry"] = {
91-
autoInstrumentServerFunctions: true,
92-
hideSourceMaps: true,
93-
};
94-
95-
plugins.push(withSentryConfig);
95+
plugins.push((nextConfig) =>
96+
withSentryConfig(nextConfig, {
97+
autoInstrumentServerFunctions: true,
98+
hideSourceMaps: true,
99+
})
100+
);
96101
}
97102

98103
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);

apps/api/v1/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@calcom/lib": "*",
3131
"@calcom/prisma": "*",
3232
"@calcom/trpc": "*",
33-
"@sentry/nextjs": "^7.73.0",
33+
"@sentry/nextjs": "^8.8.0",
3434
"bcryptjs": "^2.4.3",
3535
"memory-cache": "^0.2.0",
3636
"next": "^13.5.4",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { NextApiRequest } from "next";
2+
3+
import {
4+
getTranscriptsAccessLinkFromRecordingId,
5+
checkIfRoomNameMatchesInRecording,
6+
} from "@calcom/core/videoClient";
7+
import { HttpError } from "@calcom/lib/http-error";
8+
import { defaultResponder } from "@calcom/lib/server";
9+
import prisma from "@calcom/prisma";
10+
import type { PartialReference } from "@calcom/types/EventManager";
11+
12+
import { getTranscriptFromRecordingId } from "~/lib/validations/shared/queryIdTransformParseInt";
13+
14+
/**
15+
* @swagger
16+
* /bookings/{id}/transcripts/{recordingId}:
17+
* get:
18+
* summary: Find all Cal video transcripts of that recording
19+
* operationId: getTranscriptsByRecordingId
20+
* parameters:
21+
* - in: path
22+
* name: id
23+
* schema:
24+
* type: integer
25+
* required: true
26+
* description: ID of the booking for which transcripts need to be fetched.
27+
* - in: path
28+
* name: recordingId
29+
* schema:
30+
* type: string
31+
* required: true
32+
* description: ID of the recording(daily.co recording id) for which transcripts need to be fetched.
33+
* - in: query
34+
* name: apiKey
35+
* required: true
36+
* schema:
37+
* type: string
38+
* description: Your API key
39+
* tags:
40+
* - bookings
41+
* responses:
42+
* 200:
43+
* description: OK
44+
* content:
45+
* application/json:
46+
* 401:
47+
* description: Authorization information is missing or invalid.
48+
* 404:
49+
* description: Booking was not found
50+
*/
51+
52+
export async function getHandler(req: NextApiRequest) {
53+
const { query } = req;
54+
const { id, recordingId } = getTranscriptFromRecordingId.parse(query);
55+
56+
await checkIfRecordingBelongsToBooking(id, recordingId);
57+
58+
const transcriptsAccessLinks = await getTranscriptsAccessLinkFromRecordingId(recordingId);
59+
60+
return transcriptsAccessLinks;
61+
}
62+
63+
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
64+
const booking = await prisma.booking.findUnique({
65+
where: { id: bookingId },
66+
include: { references: true },
67+
});
68+
69+
if (!booking)
70+
throw new HttpError({
71+
statusCode: 404,
72+
message: `No Booking found with booking id ${bookingId}`,
73+
});
74+
75+
const roomName =
76+
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
77+
undefined;
78+
79+
if (!roomName)
80+
throw new HttpError({
81+
statusCode: 404,
82+
message: `No Booking Reference with Daily Video found with booking id ${bookingId}`,
83+
});
84+
85+
const canUserAccessRecordingId = await checkIfRoomNameMatchesInRecording(roomName, recordingId);
86+
if (!canUserAccessRecordingId) {
87+
throw new HttpError({
88+
statusCode: 403,
89+
message: `This Recording Id ${recordingId} does not belong to booking ${bookingId}`,
90+
});
91+
}
92+
};
93+
94+
export default defaultResponder(getHandler);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
3+
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
4+
5+
import { withMiddleware } from "~/lib/helpers/withMiddleware";
6+
7+
import authMiddleware from "../../_auth-middleware";
8+
9+
export default withMiddleware()(
10+
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
11+
await authMiddleware(req);
12+
return defaultHandler({
13+
GET: import("./_get"),
14+
})(req, res);
15+
})
16+
);

apps/api/v1/pages/api/me/_get.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user";
77

88
async function handler({ userId }: NextApiRequest) {
99
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
10-
return { user: schemaUserReadPublic.parse(data) };
10+
return {
11+
user: schemaUserReadPublic.parse({
12+
...data,
13+
avatar: data.avatarUrl,
14+
}),
15+
};
1116
}
1217

1318
export default defaultResponder(handler);

apps/api/v1/pages/api/users/[userId]/_get.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ export async function getHandler(req: NextApiRequest) {
4545
if (!isSystemWideAdmin && query.userId !== req.userId)
4646
throw new HttpError({ statusCode: 403, message: "Forbidden" });
4747
const data = await prisma.user.findUnique({ where: { id: query.userId } });
48-
const user = schemaUserReadPublic.parse(data);
48+
const user = schemaUserReadPublic.parse({
49+
...data,
50+
avatar: data?.avatarUrl,
51+
});
4952
return { user };
5053
}
5154

apps/api/v1/pages/api/users/[userId]/_patch.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import type { NextApiRequest } from "next";
22

33
import { HttpError } from "@calcom/lib/http-error";
44
import { defaultResponder } from "@calcom/lib/server";
5+
import { uploadAvatar } from "@calcom/lib/server/avatar";
56
import prisma from "@calcom/prisma";
7+
import type { Prisma } from "@calcom/prisma/client";
68

79
import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
810
import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validations/user";
@@ -101,7 +103,8 @@ export async function patchHandler(req: NextApiRequest) {
101103
if (!isSystemWideAdmin && query.userId !== req.userId)
102104
throw new HttpError({ statusCode: 403, message: "Forbidden" });
103105

104-
const body = await schemaUserEditBodyParams.parseAsync(req.body);
106+
const { avatar, ...body }: { avatar?: string | undefined } & Prisma.UserUpdateInput =
107+
await schemaUserEditBodyParams.parseAsync(req.body);
105108
// disable role or branding changes unless admin.
106109
if (!isSystemWideAdmin) {
107110
if (body.role) body.role = undefined;
@@ -119,6 +122,14 @@ export async function patchHandler(req: NextApiRequest) {
119122
message: "Bad request: Invalid default schedule id",
120123
});
121124
}
125+
126+
if (avatar) {
127+
body.avatarUrl = await uploadAvatar({
128+
userId: query.userId,
129+
avatar: await (await import("@calcom/lib/server/resizeBase64Image")).resizeBase64Image(avatar),
130+
});
131+
}
132+
122133
const data = await prisma.user.update({
123134
where: { id: query.userId },
124135
data: body,

apps/api/v1/sentry.edge.config.ts

-5
This file was deleted.

apps/api/v1/sentry.server.config.ts

-6
This file was deleted.

0 commit comments

Comments
 (0)