Skip to content

Commit 99523f1

Browse files
authored
Merge branch 'main' into devin/test-unpublished-platform-org-credit-usage-1752689519
2 parents edc03a4 + 9d65b6f commit 99523f1

21 files changed

Lines changed: 244 additions & 60 deletions

File tree

.yarn/versions/9511a7f4.yml

Whitespace-only changes.

apps/api/v2/src/modules/auth/guards/permissions/permissions.guard.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
22
import { OAuthClientsOutputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-output.service";
33
import { TokensRepository } from "@/modules/tokens/tokens.repository";
4+
import { TokensService } from "@/modules/tokens/tokens.service";
45
import { createMock } from "@golevelup/ts-jest";
56
import { ExecutionContext } from "@nestjs/common";
67
import { ConfigService } from "@nestjs/config";
@@ -19,6 +20,7 @@ describe("PermissionsGuard", () => {
1920
guard = new PermissionsGuard(
2021
reflector,
2122
createMock<TokensRepository>(),
23+
createMock<TokensService>(),
2224
createMock<ConfigService>({
2325
get: jest.fn().mockImplementation((key: string) => {
2426
switch (key) {
@@ -118,6 +120,7 @@ describe("PermissionsGuard", () => {
118120
jest
119121
.spyOn(guard, "getOAuthClientByAccessToken")
120122
.mockResolvedValue(getMockOAuthClient(oAuthClientPermissions));
123+
jest.spyOn(guard, "getDecodedThirdPartyAccessToken").mockReturnValue(null);
121124

122125
await expect(guard.canActivate(mockContext)).rejects.toThrow(
123126
"PermissionsGuard - oAuth client with id=100 does not have the required permissions=SCHEDULE_WRITE"
@@ -133,11 +136,22 @@ describe("PermissionsGuard", () => {
133136
jest
134137
.spyOn(guard, "getOAuthClientByAccessToken")
135138
.mockResolvedValue(getMockOAuthClient(oAuthClientPermissions));
139+
jest.spyOn(guard, "getDecodedThirdPartyAccessToken").mockReturnValue(null);
136140

137141
await expect(guard.canActivate(mockContext)).rejects.toThrow(
138142
"PermissionsGuard - oAuth client with id=100 does not have the required permissions=SCHEDULE_WRITE, SCHEDULE_READ"
139143
);
140144
});
145+
146+
it("should return true for 3rd party access token", async () => {
147+
const mockContext = createMockExecutionContext({ Authorization: "Bearer token" });
148+
jest.spyOn(guard, "getDecodedThirdPartyAccessToken").mockReturnValue({
149+
scope: ["scope"],
150+
token_type: "Bearer",
151+
});
152+
153+
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
154+
});
141155
});
142156

143157
describe("when OAuth id is provided", () => {

apps/api/v2/src/modules/auth/guards/permissions/permissions.guard.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
33
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
44
import { OAuthClientsOutputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-output.service";
55
import { TokensRepository } from "@/modules/tokens/tokens.repository";
6+
import { TokensService } from "@/modules/tokens/tokens.service";
67
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
78
import { ConfigService } from "@nestjs/config";
89
import { Reflector } from "@nestjs/core";
@@ -17,6 +18,7 @@ export class PermissionsGuard implements CanActivate {
1718
constructor(
1819
private reflector: Reflector,
1920
private tokensRepository: TokensRepository,
21+
private tokensService: TokensService,
2022
private readonly config: ConfigService,
2123
private readonly oAuthClientRepository: OAuthClientRepository,
2224
private readonly oAuthClientsOutputService: OAuthClientsOutputService
@@ -35,9 +37,10 @@ export class PermissionsGuard implements CanActivate {
3537
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
3638
const oAuthClientId = request.params?.clientId || request.get(X_CAL_CLIENT_ID);
3739
const apiKey = bearerToken && isApiKey(bearerToken, this.config.get("api.apiKeyPrefix") ?? "cal_");
40+
const isThirdPartyBearerToken = bearerToken && this.getDecodedThirdPartyAccessToken(bearerToken);
3841

39-
// only check permissions for accessTokens attached to an oAuth Client or oAuth credentials, not for next token or api key
40-
if (nextAuthToken || apiKey) {
42+
// only check permissions for accessTokens attached to platform oAuth Client or platform oAuth credentials, not for next token or api key or third party oauth client
43+
if (nextAuthToken || apiKey || isThirdPartyBearerToken) {
4144
return true;
4245
}
4346

@@ -87,4 +90,8 @@ export class PermissionsGuard implements CanActivate {
8790
}
8891
return oAuthClient;
8992
}
93+
94+
getDecodedThirdPartyAccessToken(bearerToken: string) {
95+
return this.tokensService.getDecodedThirdPartyAccessToken(bearerToken);
96+
}
9097
}

apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
99
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
1010
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
1111
import { ProfilesModule } from "@/modules/profiles/profiles.module";
12-
import { TokensRepository } from "@/modules/tokens/tokens.repository";
12+
import { TokensModule } from "@/modules/tokens/tokens.module";
1313
import { UsersService } from "@/modules/users/services/users.service";
1414
import { UsersRepository } from "@/modules/users/users.repository";
1515
import { ExecutionContext, HttpException } from "@nestjs/common";
@@ -64,6 +64,7 @@ describe("ApiAuthStrategy", () => {
6464
load: [appConfig],
6565
}),
6666
ProfilesModule,
67+
TokensModule,
6768
],
6869
providers: [
6970
MockedRedisService,
@@ -77,7 +78,6 @@ describe("ApiAuthStrategy", () => {
7778
OAuthClientRepository,
7879
PrismaReadService,
7980
PrismaWriteService,
80-
TokensRepository,
8181
JwtService,
8282
DeploymentsRepository,
8383
NestJwtService,

apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,20 @@ import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repo
88
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
99
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
1010
import { TokensRepository } from "@/modules/tokens/tokens.repository";
11+
import { TokensService } from "@/modules/tokens/tokens.service";
1112
import { UsersService } from "@/modules/users/services/users.service";
1213
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
13-
import {
14-
HttpException,
15-
Injectable,
16-
InternalServerErrorException,
17-
UnauthorizedException,
18-
} from "@nestjs/common";
14+
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
1915
import { Logger } from "@nestjs/common";
2016
import { ConfigService } from "@nestjs/config";
2117
import { PassportStrategy } from "@nestjs/passport";
2218
import type { Request } from "express";
23-
import * as jwt from "jsonwebtoken";
2419
import { getToken } from "next-auth/jwt";
2520

2621
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
2722

2823
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
2924

30-
interface OAuthTokenPayload {
31-
userId?: number;
32-
teamId?: number;
33-
scope: string[];
34-
token_type: string;
35-
}
36-
3725
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
3826
export type ApiAuthGuardRequest = Request & {
3927
authMethod: AuthMethods;
@@ -53,6 +41,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
5341
private readonly config: ConfigService,
5442
private readonly oauthFlowService: OAuthFlowService,
5543
private readonly tokensRepository: TokensRepository,
44+
private readonly tokensService: TokensService,
5645
private readonly userRepository: UsersRepository,
5746
private readonly apiKeyRepository: ApiKeysRepository,
5847
private readonly oauthRepository: OAuthClientRepository,
@@ -323,20 +312,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
323312
token: string,
324313
request: ApiAuthGuardRequest
325314
): Promise<{ success: true; data: UserWithProfile } | { success: false }> {
326-
// Removed requiredScopes parameter
327-
const encryptionKey = this.config.get<string>("CALENDSO_ENCRYPTION_KEY");
328-
if (!encryptionKey) {
329-
throw new InternalServerErrorException("CALENDSO_ENCRYPTION_KEY environment variable is not set.");
330-
}
331-
332-
let decodedToken: OAuthTokenPayload;
333-
try {
334-
decodedToken = jwt.verify(token, encryptionKey) as OAuthTokenPayload;
335-
} catch (e) {
336-
return { success: false };
337-
}
338-
339-
if (!decodedToken || decodedToken.token_type !== "Access Token") {
315+
const decodedToken = this.tokensService.getDecodedThirdPartyAccessToken(token);
316+
if (!decodedToken) {
340317
return { success: false };
341318
}
342319

apps/api/v2/src/modules/cal-unified-calendars/cal-unified-calendars.module.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,19 @@ import { CredentialsRepository } from "@/modules/credentials/credentials.reposit
99
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
1010
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
1111
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
12-
import { TokensRepository } from "@/modules/tokens/tokens.repository";
12+
import { TokensModule } from "@/modules/tokens/tokens.module";
1313
import { UsersRepository } from "@/modules/users/users.repository";
1414
import { Module } from "@nestjs/common";
1515

1616
@Module({
17-
imports: [],
17+
imports: [TokensModule],
1818
providers: [
1919
GCalService,
2020
GoogleCalendarService,
2121
AppsRepository,
2222
BookingReferencesRepository_2024_08_13,
2323
CredentialsRepository,
2424
CalendarsService,
25-
TokensRepository,
2625
SelectedCalendarsRepository,
2726
PrismaReadService,
2827
PrismaWriteService,
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { JwtModule } from "@/modules/jwt/jwt.module";
12
import { PrismaModule } from "@/modules/prisma/prisma.module";
23
import { TokensRepository } from "@/modules/tokens/tokens.repository";
4+
import { TokensService } from "@/modules/tokens/tokens.service";
35
import { Module } from "@nestjs/common";
46

57
@Module({
6-
imports: [PrismaModule],
7-
providers: [TokensRepository],
8-
exports: [TokensRepository],
8+
imports: [PrismaModule, JwtModule],
9+
providers: [TokensRepository, TokensService],
10+
exports: [TokensRepository, TokensService],
911
})
1012
export class TokensModule {}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Injectable, InternalServerErrorException } from "@nestjs/common";
2+
import { Logger } from "@nestjs/common";
3+
import { ConfigService } from "@nestjs/config";
4+
import * as jwt from "jsonwebtoken";
5+
6+
type OAuthTokenPayload = {
7+
userId?: number;
8+
teamId?: number;
9+
scope: string[];
10+
token_type: string;
11+
};
12+
13+
@Injectable()
14+
export class TokensService {
15+
private readonly logger = new Logger("TokensService");
16+
17+
constructor(private readonly config: ConfigService) {}
18+
19+
getDecodedThirdPartyAccessToken(token: string): OAuthTokenPayload | null {
20+
const encryptionKey = this.config.get<string>("CALENDSO_ENCRYPTION_KEY");
21+
if (!encryptionKey) {
22+
throw new InternalServerErrorException("CALENDSO_ENCRYPTION_KEY environment variable is not set.");
23+
}
24+
25+
let decodedToken: OAuthTokenPayload;
26+
try {
27+
decodedToken = jwt.verify(token, encryptionKey) as OAuthTokenPayload;
28+
} catch (e) {
29+
return null;
30+
}
31+
32+
if (!decodedToken || decodedToken.token_type !== "Access Token") {
33+
return null;
34+
}
35+
36+
return decodedToken;
37+
}
38+
}

apps/web/components/EnterprisePage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default function EnterprisePage() {
5454
buttons={
5555
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
5656
<ButtonGroup>
57-
<Button color="primary" href="https://i.cal.com/sales/enterprise?duration=25" target="_blank">
57+
<Button color="primary" href="https://go.cal.com/quote" target="_blank">
5858
{t("contact_sales")}
5959
</Button>
6060
<Button color="minimal" href="https://cal.com/enterprise" target="_blank">

apps/web/components/settings/platform/pricing/platform-pricing/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const PlatformPricing = ({ teamId, teamPlan, heading }: PlatformPricingPr
4141

4242
const handleStripeSubscription = async (plan: string) => {
4343
if (plan === "Enterprise") {
44-
router.push("https://i.cal.com/sales/exploration");
44+
return router.push("https://go.cal.com/quote");
4545
}
4646

4747
if (currentPage === "platform") {

0 commit comments

Comments
 (0)