Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
49 changes: 49 additions & 0 deletions .agents/knowledge-base.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,55 @@ dates.map((date) => dayjs.utc(date).add(1, "day").format());
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
```

## Next.js App Directory: Authorisation Checks in Pages

This can include checking session.user exists or session.org etc.

### TL;DR:

Don’t put permission checks in layout.tsx! Always put them directly inside your page.tsx or relevant server components for every restricted route.


### Why Not to Use Layouts for Permission Checks

* Layouts don’t intercept all requests: If a user navigates directly or refreshes a protected route, layout checks might be skipped, exposing sensitive content.
* APIs and server actions bypass layouts: Sensitive operations running on the server can’t be guarded by checks in the layout.
* Risk of data leaks: Only page/server-level checks ensure that unauthorized users never get protected data.

### ✅ How To Secure Routes (The Right Way)

* Check permissions inside page.tsx or the actual server component.
* Perform all session/user/role validation before querying or rendering sensitive content.
* Redirect or return nothing to unauthorized users, before running restricted code.

### 🛠️ Example: Page-Level Permission Check


```tsx
// app/admin/page.tsx

import { redirect } from "next/navigation";
import { getUserSession } from "@/lib/auth";

export default async function AdminPage() {
const session = await getUserSession();

if (!session || session.user.role !== "admin") {
redirect("/"); // Or show an error
}

// Protected content here
return <div>Welcome, Admin!</div>;
}
```

### 🧠 Key Reminders

* Put permission guards in every restricted page.tsx.
* Never assume layouts are secure for guarding data.
* Validate users before any sensitive queries or rendering.


## Avoid using Dayjs if you don’t need to be strictly tz aware.

When doing logic like Dayjs.startOf(".."), you can instead use date-fns' `startOfMonth(dateObj)` / `endOfDay(dateObj)`;
Expand Down
16 changes: 15 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,24 @@ GOOGLE_LOGIN_ENABLED=false
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
GOOGLE_API_CREDENTIALS=

# Token to verify incoming webhooks from Google Calendar
GOOGLE_WEBHOOK_TOKEN=
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
# Optional URL to override for tunneling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
# Tip: use a strong random string for GOOGLE_WEBHOOK_TOKEN, e.g.:
# openssl rand -hex 32
GOOGLE_WEBHOOK_URL=


# - MICROSOFT WEBHOOKS *****************************************************************************************
# Token to verify incoming webhooks from Microsoft
# Generate a random secret for webhook verification, e.g., openssl rand -hex 32
MICROSOFT_WEBHOOK_TOKEN=
# Optional URL to override for tunneling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL when unset.
# Tip: use a strong random string for MICROSOFT_WEBHOOK_TOKEN, e.g.:
# openssl rand -hex 32
MICROSOFT_WEBHOOK_URL=

# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=

Expand Down Expand Up @@ -203,6 +216,7 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
ORG_MONTHLY_CREDITS=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
Expand Down
29 changes: 17 additions & 12 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ module.exports = {
],
},
},
{
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"warn",
{
paths: ["@calcom/trpc"],
patterns: ["@calcom/trpc/*"],
},
],
},
},
// { temporary removal
// files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
// rules: {
// "no-restricted-imports": [
// "error",
// {
// patterns: [
// {
// group: ["@calcom/trpc/*", "@trpc/*"],
// message: "tRPC imports are blocked in packages/app-store. Move UI to apps/web/components/apps or introduce an API boundary.",
// allowTypeImports: false,
// },
// ],
// },
// ],
// },
// },
],
};
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
"spellright.documentTypes": ["markdown", "typescriptreact"],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove

"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
}
Empty file added .yarn/versions/4eff452f.yml
Empty file.
3 changes: 3 additions & 0 deletions .yarn/versions/b0b51a46.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/app-store"
Empty file added .yarn/versions/d483660a.yml
Empty file.
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests
- `yarn e2e <filename> --grep "<testName>"` - Run specific E2E test

## Tool Preferences

### Search Tools Priority

Use tools in this order of preference:

1. **ast-grep** - For AST-based code searches (if available)
2. **rg (ripgrep)** - For fast text searches
3. **grep** - As fallback for text searches

## 📚 Detailed Documentation

- **[.agents/README.md](.agents/README.md)** - Complete development guide
Expand Down
2 changes: 1 addition & 1 deletion apps/api/v1/lib/validations/booking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const bookingCancelSchema = z.object({
id: z.number(),
allRemainingBookings: z.boolean().optional(),
cancelSubsequentBookings: z.boolean().optional(),
cancellationReason: z.string().min(1).optional(),
cancellationReason: z.string().optional().default("Not Provided"),
seatReferenceUid: z.string().optional(),
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
internalNote: z
Expand Down
7 changes: 7 additions & 0 deletions apps/api/v1/next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { withAxiom } = require("next-axiom");
const { withSentryConfig } = require("@sentry/nextjs");
const { PrismaPlugin } = require("@prisma/nextjs-monorepo-workaround-plugin");

const plugins = [withAxiom];

Expand All @@ -14,6 +15,12 @@ const nextConfig = {
"@calcom/prisma",
"@calcom/trpc",
],
webpack: (config, { isServer }) => {
if (isServer) {
config.plugins = [...config.plugins, new PrismaPlugin()];
}
return config;
},
async headers() {
return [
{
Expand Down
1 change: 1 addition & 0 deletions apps/api/v1/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/trpc": "*",
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
"@sentry/nextjs": "^9.15.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.354",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.359",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
Expand Down
2 changes: 2 additions & 0 deletions apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repo
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { ProfilesModule } from "@/modules/profiles/profiles.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
Expand All @@ -33,6 +34,7 @@ import { Module } from "@nestjs/common";
EventTypesModule_2024_04_15,
SchedulesModule_2024_04_15,
EventTypesModule_2024_06_14,
ProfilesModule,
],
providers: [
TokensRepository,
Expand Down
2 changes: 2 additions & 0 deletions apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { ProfilesModule } from "@/modules/profiles/profiles.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { StripeModule } from "@/modules/stripe/stripe.module";
Expand All @@ -56,6 +57,7 @@ import { Module } from "@nestjs/common";
TeamsModule,
TeamsEventTypesModule,
MembershipsModule,
ProfilesModule,
],
providers: [
TokensRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export class InputBookingsService_2024_08_13 {
metadata: inputBooking.metadata || {},
hasHashedBookingLink: false,
guests,
verificationCode: inputBooking.emailVerificationCode,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: {
...(inputBooking.bookingFieldsResponses || {}),
Expand Down Expand Up @@ -471,6 +472,7 @@ export class InputBookingsService_2024_08_13 {
metadata: inputBooking.metadata || {},
hasHashedBookingLink: false,
guests,
verificationCode: inputBooking.emailVerificationCode,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: {
...(inputBooking.bookingFieldsResponses || {}),
Expand Down Expand Up @@ -600,6 +602,7 @@ export class InputBookingsService_2024_08_13 {
guests: [],
responses: { ...bookingResponses },
rescheduleUid: inputBooking.seatUid,
verificationCode: inputBooking.emailVerificationCode,
};
}

Expand Down Expand Up @@ -661,6 +664,7 @@ export class InputBookingsService_2024_08_13 {
guests: bookingResponses.guests,
responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason },
rescheduleUid: bookingUid,
verificationCode: inputBooking.emailVerificationCode,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ import {
APPLE_CALENDAR,
CREDENTIAL_CALENDARS,
} from "@calcom/platform-constants";
import type {
ApiResponse,
CalendarBusyTimesInput,
CreateCalendarCredentialsInput,
} from "@calcom/platform-types";
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
import type { User } from "@calcom/prisma/client";

export interface CalendarState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,9 @@ describe("Event types Endpoints", () => {
disableRecordingForGuests: true,
disableRecordingForOrganizer: true,
enableAutomaticRecordingForOrganizer: true,
enableAutomaticTranscription: true,
disableTranscriptionForGuests: true,
disableTranscriptionForOrganizer: true,
},
bookingFields: [
nameBookingField,
Expand Down Expand Up @@ -1274,6 +1277,15 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.calVideoSettings?.enableAutomaticRecordingForOrganizer).toEqual(
body.calVideoSettings?.enableAutomaticRecordingForOrganizer
);
expect(updatedEventType.calVideoSettings?.enableAutomaticTranscription).toEqual(
body.calVideoSettings?.enableAutomaticTranscription
);
expect(updatedEventType.calVideoSettings?.disableTranscriptionForGuests).toEqual(
body.calVideoSettings?.disableTranscriptionForGuests
);
expect(updatedEventType.calVideoSettings?.disableTranscriptionForOrganizer).toEqual(
body.calVideoSettings?.disableTranscriptionForOrganizer
);

eventType.title = newTitle;
eventType.scheduleId = secondSchedule.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ export class AtomsVerificationController {
@Version(VERSION_NEUTRAL)
@HttpCode(HttpStatus.OK)
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
await this.verificationService.verifyEmailCodeUnAuthenticated({
email: body.email,
code: body.code,
});

return {
data: { verified },
data: { verified: true },
status: SUCCESS_STATUS,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class VerificationAtomsService {

async verifyEmailCodeUnAuthenticated(input: VerifyEmailCodeInput) {
try {
return await verifyCodeUnAuthenticated(input);
return await verifyCodeUnAuthenticated(input.email, input.code);
} catch (error) {
if (error instanceof Error) {
if (error.message === "invalid_code") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { NO_AUTH_PROVIDED_MESSAGE } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import {
NO_AUTH_PROVIDED_MESSAGE,
ONLY_CLIENT_ID_PROVIDED_MESSAGE,
} from "@/modules/auth/strategies/api-auth/api-auth.strategy";

export class OptionalApiAuthGuard extends ApiAuthGuard {
handleRequest(error: Error, user: any) {
// note(Lauris): optional means that auth is not required but if it is invalid then still throw error.
const noAuthProvided = error && error.message.includes(NO_AUTH_PROVIDED_MESSAGE);
const onlyClientIdProvided = error && error.message.includes(ONLY_CLIENT_ID_PROVIDED_MESSAGE);

if (onlyClientIdProvided) {
return null;
}

if (user || noAuthProvided || !error) {
return user || null;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export type ApiAuthGuardRequest = Request & {
export const NO_AUTH_PROVIDED_MESSAGE =
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";

export const ONLY_CLIENT_ID_PROVIDED_MESSAGE =
"Only 'x-cal-client-id' header provided. Please also provide 'x-cal-secret-key' header or Auth bearer token as 'Authentication' header";

@Injectable()
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
private readonly logger = new Logger("ApiAuthStrategy");
Expand Down Expand Up @@ -117,10 +120,15 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
}

const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
const onlyClientIdProvided = !!oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
if (noAuthProvided) {
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
}

if (onlyClientIdProvided) {
throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_ID_PROVIDED_MESSAGE}`);
}

throw new UnauthorizedException(
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${
allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
HttpStatus,
Logger,
Delete,
BadRequestException,
ParseIntPipe,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiExcludeController } from "@nestjs/swagger";
Expand Down Expand Up @@ -51,7 +51,7 @@ export class BillingController {
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@MembershipRoles(["OWNER", "ADMIN", "MEMBER"])
async checkTeamBilling(
@Param("teamId") teamId: number
@Param("teamId", ParseIntPipe) teamId: number
): Promise<ApiResponse<CheckPlatformBillingResponseDto>> {
const { status, plan } = await this.billingService.getBillingData(teamId);

Expand Down
Loading
Loading