chore(platform): OAuth Flow (#12798)

This commit is contained in:
Erik 2023-12-20 15:03:26 -03:00 committed by GitHub
parent b987f6ea4d
commit 0830f3304e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 618 additions and 223 deletions

View File

@ -2,7 +2,7 @@
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],

View File

@ -40,6 +40,7 @@
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"helmet": "^7.1.0",
"luxon": "^3.4.4",
"nest-winston": "^1.9.4",
"next-auth": "^4.24.5",
"passport": "^0.7.0",
@ -56,6 +57,7 @@
"@types/cookie-parser": "^1.4.6",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/luxon": "^3.3.7",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.12",

View File

@ -0,0 +1,19 @@
import { OAuthFlowService } from "@/modules/oauth/flow/oauth-flow.service";
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
@Injectable()
export class AccessTokenGuard implements CanActivate {
constructor(private readonly oauthFlowService: OAuthFlowService) {}
canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
const bearer = authHeader.replace("Bearer ", "").trim();
if (!bearer) {
throw new UnauthorizedException();
}
return this.oauthFlowService.validateAccessToken(bearer);
}
}

View File

@ -1,10 +1,11 @@
import { BookingModule } from "@/modules/booking/booking.module";
import { OAuthFlowModule } from "@/modules/oauth/flow/oauth-flow.module";
import { OAuthClientModule } from "@/modules/oauth/oauth-client.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@Module({
imports: [BookingModule, OAuthClientModule],
imports: [BookingModule, OAuthClientModule, OAuthFlowModule],
})
export class EndpointsModule implements NestModule {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -0,0 +1,9 @@
import { IsString } from "class-validator";
export class OAuthAuthorizeInput {
@IsString()
redirect_uri!: string;
@IsString()
client_id!: string;
}

View File

@ -0,0 +1,9 @@
import { IsString } from "class-validator";
export class ExchangeAuthorizationCodeInput {
@IsString()
client_id!: string;
@IsString()
client_secret!: string;
}

View File

@ -0,0 +1,6 @@
import { IsString } from "class-validator";
export class RefreshTokenInput {
@IsString()
refresh_token!: string;
}

View File

@ -0,0 +1,106 @@
import { GetUser } from "@/modules/auth/decorator";
import { NextAuthGuard } from "@/modules/auth/guard";
import { OAuthAuthorizeInput } from "@/modules/oauth/flow/input/authorize.input";
import { ExchangeAuthorizationCodeInput } from "@/modules/oauth/flow/input/exchange-code.input";
import { RefreshTokenInput } from "@/modules/oauth/flow/input/refresh-token.input";
import { OAuthFlowService } from "@/modules/oauth/flow/oauth-flow.service";
import { OAuthClientGuard } from "@/modules/oauth/guard/oauth-client/oauth-client.guard";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import {
BadRequestException,
Body,
Controller,
Headers,
HttpCode,
HttpStatus,
Post,
Response,
UseGuards,
} from "@nestjs/common";
import { Response as ExpressResponse } from "express";
import { SUCCESS_STATUS, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
path: "oauth",
version: "2",
})
export class OAuthFlowController {
constructor(
private readonly oauthClientRepository: OAuthClientRepository,
private readonly tokensRepository: TokensRepository,
private readonly oAuthFlowService: OAuthFlowService
) {}
@Post("/authorize")
@HttpCode(HttpStatus.OK)
@UseGuards(NextAuthGuard)
async authorize(
@Body() body: OAuthAuthorizeInput,
@GetUser("id") userId: number,
@Response() res: ExpressResponse
): Promise<void> {
const oauthClient = await this.oauthClientRepository.getOAuthClient(body.client_id);
if (!oauthClient) {
throw new BadRequestException();
}
if (!oauthClient?.redirect_uris.includes(body.redirect_uri)) {
throw new BadRequestException("Invalid 'redirect_uri' value.");
}
const { id } = await this.tokensRepository.createAuthorizationToken(body.client_id, userId);
return res.redirect(`${body.redirect_uri}?code=${id}`);
}
@Post("/exchange")
@HttpCode(HttpStatus.OK)
async exchange(
@Headers("Authorization") authorization: string,
@Body() body: ExchangeAuthorizationCodeInput
): Promise<ApiResponse<{ access_token: string; refresh_token: string }>> {
const bearerToken = authorization.replace("Bearer ", "").trim();
if (!bearerToken) {
throw new BadRequestException("Missing 'Bearer' Authorization header.");
}
const { access_token, refresh_token } = await this.oAuthFlowService.exchangeAuthorizationToken(
bearerToken,
body
);
return {
status: SUCCESS_STATUS,
data: {
access_token,
refresh_token,
},
};
}
@Post("/refresh")
@HttpCode(HttpStatus.OK)
@UseGuards(OAuthClientGuard)
async refreshAccessToken(
@Headers(X_CAL_CLIENT_ID) clientId: string,
@Headers(X_CAL_SECRET_KEY) secretKey: string,
@Body() body: RefreshTokenInput
): Promise<ApiResponse<{ access_token: string; refresh_token: string }>> {
const { access_token, refresh_token } = await this.oAuthFlowService.refreshToken(
clientId,
secretKey,
body.refresh_token
);
return {
status: SUCCESS_STATUS,
data: {
access_token,
refresh_token,
},
};
}
}

View File

@ -0,0 +1,21 @@
import { getEnv } from "@/env";
import { OAuthFlowController } from "@/modules/oauth/flow/oauth-flow.controller";
import { OAuthFlowService } from "@/modules/oauth/flow/oauth-flow.service";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
@Module({
imports: [
JwtModule.register({
secret: getEnv("NEXTAUTH_SECRET"),
}),
PrismaModule,
],
controllers: [OAuthFlowController],
providers: [TokensRepository, OAuthClientRepository, OAuthFlowService],
exports: [OAuthFlowService],
})
export class OAuthFlowModule {}

View File

@ -0,0 +1,107 @@
import { ExchangeAuthorizationCodeInput } from "@/modules/oauth/flow/input/exchange-code.input";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
@Injectable()
export class OAuthFlowService {
private logger = new Logger("OAuthFlowService");
constructor(
private readonly tokensRepository: TokensRepository,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async propagateAccessToken(accessToken: string) {
this.logger.log("Propagating access token to redis", accessToken);
// TODO propagate
return void 0;
}
async validateAccessToken(secret: string) {
// status can be "CACHE_HIT" or "CACHE_MISS", MISS will most likely mean the token has expired
// but we need to check the SQL db for it anyways.
const { status } = await this.readFromCache(secret);
if (status === "CACHE_HIT") {
return true;
}
const token = await this.tokensRepository.getAccessTokenBySecret(secret);
if (!token) {
throw new UnauthorizedException();
}
if (new Date() > token?.expiresAt) {
throw new BadRequestException("Token is expired");
}
return true;
}
private async readFromCache(secret: string) {
return { status: "CACHE_MISS" };
}
async exchangeAuthorizationToken(
tokenId: string,
input: ExchangeAuthorizationCodeInput
): Promise<{ access_token: string; refresh_token: string }> {
const oauthClient = await this.oAuthClientRepository.getOAuthClientWithAuthTokens(
tokenId,
input.client_id,
input.client_secret
);
if (!oauthClient) {
throw new BadRequestException("Invalid OAuth Client.");
}
const authorizationToken = oauthClient.authorizationTokens[0];
if (!authorizationToken || !authorizationToken.owner.id) {
throw new BadRequestException("Invalid Authorization Token.");
}
const { access_token, refresh_token } = await this.tokensRepository.createOAuthTokens(
input.client_id,
authorizationToken.owner.id
);
void this.propagateAccessToken(access_token); // voided as we don't need to await
return {
access_token,
refresh_token,
};
}
async refreshToken(clientId: string, clientSecret: string, tokenSecret: string) {
const oauthClient = await this.oAuthClientRepository.getOAuthClientWithRefreshSecret(
clientId,
clientSecret,
tokenSecret
);
if (!oauthClient) {
throw new BadRequestException("Invalid OAuthClient credentials.");
}
const currentRefreshToken = oauthClient.refreshToken[0];
if (!currentRefreshToken) {
throw new BadRequestException("Invalid refresh token");
}
const { accessToken, refreshToken } = await this.tokensRepository.refreshOAuthTokens(
clientId,
currentRefreshToken.secret,
currentRefreshToken.userId
);
return {
access_token: accessToken.secret,
refresh_token: refreshToken.secret,
};
}
}

View File

@ -30,6 +30,50 @@ export class OAuthClientRepository {
});
}
async getOAuthClientWithAuthTokens(tokenId: string, clientId: string, clientSecret: string) {
return this.dbRead.prisma.platformOAuthClient.findUnique({
where: {
id: clientId,
secret: clientSecret,
authorizationTokens: {
some: {
id: tokenId,
},
},
},
include: {
authorizationTokens: {
where: {
id: tokenId,
},
include: {
owner: {
select: {
id: true,
},
},
},
},
},
});
}
async getOAuthClientWithRefreshSecret(clientId: string, clientSecret: string, refreshToken: string) {
return await this.dbRead.prisma.platformOAuthClient.findFirst({
where: {
id: clientId,
secret: clientSecret,
},
include: {
refreshToken: {
where: {
secret: refreshToken,
},
},
},
});
}
async getOrganizationOAuthClients(organizationId: number): Promise<PlatformOAuthClient[]> {
return this.dbRead.prisma.platformOAuthClient.findMany({
where: {

View File

@ -0,0 +1,17 @@
import { getEnv } from "@/env";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
@Module({
imports: [
JwtModule.register({
secret: getEnv("NEXTAUTH_SECRET"),
}),
PrismaModule,
],
providers: [TokensRepository],
exports: [TokensRepository],
})
export class TokensModule {}

View File

@ -0,0 +1,102 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { PlatformAuthorizationToken } from "@prisma/client";
import { DateTime } from "luxon";
@Injectable()
export class TokensRepository {
constructor(
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private readonly jwtService: JwtService
) {}
async createAuthorizationToken(clientId: string, userId: number): Promise<PlatformAuthorizationToken> {
return this.dbWrite.prisma.platformAuthorizationToken.create({
data: {
client: {
connect: {
id: clientId,
},
},
owner: {
connect: {
id: userId,
},
},
},
});
}
async createOAuthTokens(clientId: string, ownerId: number) {
const accessExpiry = DateTime.now().plus({ days: 1 }).startOf("day").toJSDate();
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
this.dbWrite.prisma.accessToken.create({
data: {
secret: this.jwtService.sign(JSON.stringify({ type: "access_token", clientId })),
expiresAt: accessExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: ownerId } },
},
}),
this.dbWrite.prisma.refreshToken.create({
data: {
secret: this.jwtService.sign(JSON.stringify({ type: "refresh_token", clientId })),
expiresAt: refreshExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: ownerId } },
},
}),
]);
return {
access_token: accessToken.secret,
refresh_token: refreshToken.secret,
};
}
async getAccessTokenBySecret(secret: string) {
return this.dbRead.prisma.accessToken.findFirst({
where: {
secret,
},
select: {
expiresAt: true,
},
});
}
async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) {
const accessExpiry = DateTime.now().plus({ days: 1 }).startOf("day").toJSDate();
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, _refresh, accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
this.dbWrite.prisma.accessToken.deleteMany({
where: { client: { id: clientId }, expiresAt: { lte: new Date() } },
}),
this.dbWrite.prisma.refreshToken.delete({ where: { secret: refreshTokenSecret } }),
this.dbWrite.prisma.accessToken.create({
data: {
secret: this.jwtService.sign(JSON.stringify({ type: "access_token", clientId: clientId })),
expiresAt: accessExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: tokenUserId } },
},
}),
this.dbWrite.prisma.refreshToken.create({
data: {
secret: this.jwtService.sign(JSON.stringify({ type: "refresh_token", clientId: clientId })),
expiresAt: refreshExpiry,
client: { connect: { id: clientId } },
owner: { connect: { id: tokenUserId } },
},
}),
]);
return { accessToken, refreshToken };
}
}

View File

@ -0,0 +1,93 @@
-- CreateTable
CREATE TABLE "PlatformOAuthClient" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"secret" TEXT NOT NULL,
"permissions" INTEGER NOT NULL,
"logo" TEXT,
"redirect_uris" TEXT[],
"organizationId" INTEGER NOT NULL,
CONSTRAINT "PlatformOAuthClient_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "platform_authorization_token" (
"id" TEXT NOT NULL,
"platformOAuthClientId" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "platform_authorization_token_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "platform_access_tokens" (
"id" SERIAL NOT NULL,
"secret" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
"platformOAuthClientId" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "platform_access_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "platform_refresh_token" (
"id" SERIAL NOT NULL,
"secret" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
"platformOAuthClientId" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "platform_refresh_token_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_PlatformOAuthClientToUser" (
"A" TEXT NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "platform_authorization_token_userId_platformOAuthClientId_key" ON "platform_authorization_token"("userId", "platformOAuthClientId");
-- CreateIndex
CREATE UNIQUE INDEX "platform_access_tokens_secret_key" ON "platform_access_tokens"("secret");
-- CreateIndex
CREATE UNIQUE INDEX "platform_refresh_token_secret_key" ON "platform_refresh_token"("secret");
-- CreateIndex
CREATE UNIQUE INDEX "_PlatformOAuthClientToUser_AB_unique" ON "_PlatformOAuthClientToUser"("A", "B");
-- CreateIndex
CREATE INDEX "_PlatformOAuthClientToUser_B_index" ON "_PlatformOAuthClientToUser"("B");
-- AddForeignKey
ALTER TABLE "PlatformOAuthClient" ADD CONSTRAINT "PlatformOAuthClient_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "platform_authorization_token" ADD CONSTRAINT "platform_authorization_token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "platform_authorization_token" ADD CONSTRAINT "platform_authorization_token_platformOAuthClientId_fkey" FOREIGN KEY ("platformOAuthClientId") REFERENCES "PlatformOAuthClient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "platform_access_tokens" ADD CONSTRAINT "platform_access_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "platform_access_tokens" ADD CONSTRAINT "platform_access_tokens_platformOAuthClientId_fkey" FOREIGN KEY ("platformOAuthClientId") REFERENCES "PlatformOAuthClient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "platform_refresh_token" ADD CONSTRAINT "platform_refresh_token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "platform_refresh_token" ADD CONSTRAINT "platform_refresh_token_platformOAuthClientId_fkey" FOREIGN KEY ("platformOAuthClientId") REFERENCES "PlatformOAuthClient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_PlatformOAuthClientToUser" ADD CONSTRAINT "_PlatformOAuthClientToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "PlatformOAuthClient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_PlatformOAuthClientToUser" ADD CONSTRAINT "_PlatformOAuthClientToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -268,8 +268,11 @@ model User {
//linkedUsers User[] @relation("linked_account")*/
// Used to lock the user account
locked Boolean @default(false)
platformOAuthClients PlatformOAuthClient[]
locked Boolean @default(false)
platformOAuthClients PlatformOAuthClient[]
AccessToken AccessToken[]
RefreshToken RefreshToken[]
PlatformAuthorizationToken PlatformAuthorizationToken[]
@@unique([email])
@@unique([email, username])
@ -1044,12 +1047,60 @@ model Avatar {
model PlatformOAuthClient {
id String @id @default(cuid())
name String
secret String
secret String // auth secret?
permissions Int
users User[]
logo String?
redirect_uris String[]
organizationId Int
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
// add connection to Access Codes
accessTokens AccessToken[]
refreshToken RefreshToken[]
authorizationTokens PlatformAuthorizationToken[]
}
model PlatformAuthorizationToken {
id String @id @default(cuid())
owner User @relation(fields: [userId], references: [id])
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id])
platformOAuthClientId String
userId Int
@@unique([userId, platformOAuthClientId])
@@map("platform_authorization_token")
}
model AccessToken {
id Int @id @default(autoincrement())
secret String @unique
createdAt DateTime @default(now())
expiresAt DateTime
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
platformOAuthClientId String
userId Int
@@map("platform_access_tokens")
}
model RefreshToken {
id Int @id @default(autoincrement())
secret String @unique
createdAt DateTime @default(now())
expiresAt DateTime
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
platformOAuthClientId String
userId Int
@@map("platform_refresh_token")
}

242
yarn.lock
View File

@ -17,34 +17,6 @@ __metadata:
languageName: node
linkType: hard
"@47ng/cloak@npm:^1.1.0":
version: 1.1.0
resolution: "@47ng/cloak@npm:1.1.0"
dependencies:
"@47ng/codec": ^1.0.1
"@stablelib/base64": ^1.0.1
"@stablelib/hex": ^1.0.1
"@stablelib/utf8": ^1.0.1
chalk: ^4.1.2
commander: ^8.3.0
dotenv: ^10.0.0
s-ago: ^2.2.0
bin:
cloak: dist/cli.js
checksum: 7d72c66ff7837368e9ca8f5ba402d72041427eb47c53c340b4640e3352f2956d8673a4a8e97591fb2b9dfe27f3d2765bcd925617273ef2488df2565c77c78299
languageName: node
linkType: hard
"@47ng/codec@npm:^1.0.1":
version: 1.1.0
resolution: "@47ng/codec@npm:1.1.0"
dependencies:
"@stablelib/base64": ^1.0.1
"@stablelib/hex": ^1.0.1
checksum: 4f780c4413fe78bbedbaff4135340c0e5f5a30df88f5cffbec51349eb0a1c909728e6c2bbda52506ff8c12653bf39b78c67b78bbe9501b0b9741da0cdaeec6ff
languageName: node
linkType: hard
"@achrinza/event-pubsub@npm:5.0.8":
version: 5.0.8
resolution: "@achrinza/event-pubsub@npm:5.0.8"
@ -3842,6 +3814,7 @@ __metadata:
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
"@calcom/prisma": "*"
"@golevelup/ts-jest": ^0.4.0
"@nestjs/cli": ^10.0.0
"@nestjs/common": ^10.0.0
"@nestjs/config": ^3.1.1
@ -3857,6 +3830,7 @@ __metadata:
"@types/cookie-parser": ^1.4.6
"@types/express": ^4.17.17
"@types/jest": ^29.5.2
"@types/luxon": ^3.3.7
"@types/node": ^20.3.1
"@types/passport-jwt": ^3.0.13
"@types/supertest": ^2.0.12
@ -3866,6 +3840,7 @@ __metadata:
dotenv: ^16.3.1
helmet: ^7.1.0
jest: ^29.5.0
luxon: ^3.4.4
nest-winston: ^1.9.4
next-auth: ^4.24.5
passport: ^0.7.0
@ -4053,43 +4028,6 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/console@workspace:apps/console":
version: 0.0.0-use.local
resolution: "@calcom/console@workspace:apps/console"
dependencies:
"@calcom/dayjs": "*"
"@calcom/features": "*"
"@calcom/lib": "*"
"@calcom/tsconfig": "*"
"@calcom/ui": "*"
"@headlessui/react": ^1.5.0
"@heroicons/react": ^1.0.6
"@prisma/client": ^5.4.2
"@tailwindcss/forms": ^0.5.2
"@types/node": 16.9.1
"@types/react": 18.0.26
autoprefixer: ^10.4.12
chart.js: ^3.7.1
client-only: ^0.0.1
eslint: ^8.34.0
next: ^13.4.6
next-auth: ^4.22.1
next-i18next: ^13.2.2
postcss: ^8.4.18
prisma: ^5.4.2
prisma-field-encryption: ^1.4.0
react: ^18.2.0
react-chartjs-2: ^4.0.1
react-dom: ^18.2.0
react-hook-form: ^7.43.3
react-live-chat-loader: ^2.8.1
swr: ^1.2.2
tailwindcss: ^3.3.1
typescript: ^4.9.4
zod: ^3.22.2
languageName: unknown
linkType: soft
"@calcom/core@*, @calcom/core@workspace:packages/core":
version: 0.0.0-use.local
resolution: "@calcom/core@workspace:packages/core"
@ -6655,6 +6593,13 @@ __metadata:
languageName: node
linkType: hard
"@golevelup/ts-jest@npm:^0.4.0":
version: 0.4.0
resolution: "@golevelup/ts-jest@npm:0.4.0"
checksum: 1cb2c938771493445d478665594927304fd0890e964c8d38a11a3f8735fd83a01bed3e5d470c403d3f8f38e8b67571aab822c1bebbedc8562faf2936e74c8f66
languageName: node
linkType: hard
"@graphql-codegen/cli@npm:^5.0.0":
version: 5.0.0
resolution: "@graphql-codegen/cli@npm:5.0.0"
@ -9402,17 +9347,6 @@ __metadata:
languageName: node
linkType: hard
"@prisma/debug@npm:5.5.2":
version: 5.5.2
resolution: "@prisma/debug@npm:5.5.2"
dependencies:
"@types/debug": 4.1.9
debug: 4.3.4
strip-ansi: 6.0.1
checksum: ff082622d4ba1b6fe07edda85a7b4dfb499308857e015645b9fec2041288fb0247d73974386edda2c25bac7d5167b6008e118386f169d20215035a70d5742d2a
languageName: node
linkType: hard
"@prisma/debug@npm:5.7.0":
version: 5.7.0
resolution: "@prisma/debug@npm:5.7.0"
@ -9561,18 +9495,6 @@ __metadata:
languageName: node
linkType: hard
"@prisma/generator-helper@npm:^5.0.0":
version: 5.5.2
resolution: "@prisma/generator-helper@npm:5.5.2"
dependencies:
"@prisma/debug": 5.5.2
"@types/cross-spawn": 6.0.3
cross-spawn: 7.0.3
kleur: 4.1.5
checksum: 4aef64ba4bcf3211358148fc1dc782541a33129154e5bb86687b60aff41674e1578ac8ccadee5a9e719d4d9b304963395ae7276d8b59760dec93bfa4517dff34
languageName: node
linkType: hard
"@prisma/generator-helper@npm:^5.4.2":
version: 5.4.2
resolution: "@prisma/generator-helper@npm:5.4.2"
@ -12015,27 +11937,13 @@ __metadata:
languageName: node
linkType: hard
"@stablelib/base64@npm:^1.0.0, @stablelib/base64@npm:^1.0.1":
"@stablelib/base64@npm:^1.0.0":
version: 1.0.1
resolution: "@stablelib/base64@npm:1.0.1"
checksum: 3ef4466d1d6889ac3fc67407bc21aa079953981c322eeca3b29f426d05506c63011faab1bfc042d7406e0677a94de6c9d2db2ce079afdd1eccae90031bfb5859
languageName: node
linkType: hard
"@stablelib/hex@npm:^1.0.1":
version: 1.0.1
resolution: "@stablelib/hex@npm:1.0.1"
checksum: 557f1c5d6b42963deee7627d4be1ae3542607851c5561e9419c42682d09562ebd3a06e2d92e088c52213a71ed121ec38221abfc5acd9e65707a77ecee3c96915
languageName: node
linkType: hard
"@stablelib/utf8@npm:^1.0.1":
version: 1.0.1
resolution: "@stablelib/utf8@npm:1.0.1"
checksum: 098d9446f38a641a8ee265a7fc3467fefd561fc46ca65e1216c1df7a9b4d004e616347ce79f4b83d62e944f0f91d6be4af029ad0b027a20c3271951921ebfac5
languageName: node
linkType: hard
"@storybook/addon-actions@npm:7.6.3, @storybook/addon-actions@npm:^7.6.3":
version: 7.6.3
resolution: "@storybook/addon-actions@npm:7.6.3"
@ -14155,6 +14063,13 @@ __metadata:
languageName: node
linkType: hard
"@types/luxon@npm:^3.3.7":
version: 3.3.7
resolution: "@types/luxon@npm:3.3.7"
checksum: 97026557e92bcba308a5592f981591cd200d493fc8997874d79acecf6a2ec41debeded3ac5cd80c371ef7f6f56cc0d1be0a5aca846e03d3e6b4a2be37256fe2f
languageName: node
linkType: hard
"@types/mailparser@npm:^3.4.0":
version: 3.4.0
resolution: "@types/mailparser@npm:3.4.0"
@ -17823,13 +17738,6 @@ __metadata:
languageName: node
linkType: hard
"chart.js@npm:^3.7.1":
version: 3.9.1
resolution: "chart.js@npm:3.9.1"
checksum: 9ab0c0ac01215af0b3f020f2e313030fd6e347b48ed17d5484ee9c4e8ead45e78ae71bea16c397621c386b409ce0b14bf17f9f6c2492cd15b56c0f433efdfff6
languageName: node
linkType: hard
"check-error@npm:^1.0.3":
version: 1.0.3
resolution: "check-error@npm:1.0.3"
@ -20391,13 +20299,6 @@ __metadata:
languageName: node
linkType: hard
"dotenv@npm:^10.0.0":
version: 10.0.0
resolution: "dotenv@npm:10.0.0"
checksum: f412c5fe8c24fbe313d302d2500e247ba8a1946492db405a4de4d30dd0eb186a88a43f13c958c5a7de303938949c4231c56994f97d05c4bc1f22478d631b4005
languageName: node
linkType: hard
"dotenv@npm:^16.0.0":
version: 16.0.1
resolution: "dotenv@npm:16.0.1"
@ -24757,13 +24658,6 @@ __metadata:
languageName: node
linkType: hard
"immer@npm:^10.0.2":
version: 10.0.3
resolution: "immer@npm:10.0.3"
checksum: 76acabe6f40e752028313762ba477a5d901e57b669f3b8fb406b87b9bb9b14e663a6fbbf5a6d1ab323737dd38f4b2494a4e28002045b88948da8dbf482309f28
languageName: node
linkType: hard
"immutable@npm:^3.8.2, immutable@npm:^3.x.x":
version: 3.8.2
resolution: "immutable@npm:3.8.2"
@ -26538,15 +26432,6 @@ __metadata:
languageName: node
linkType: hard
"jiti@npm:^1.19.1":
version: 1.21.0
resolution: "jiti@npm:1.21.0"
bin:
jiti: bin/jiti.js
checksum: a7bd5d63921c170eaec91eecd686388181c7828e1fa0657ab374b9372bfc1f383cf4b039e6b272383d5cb25607509880af814a39abdff967322459cca41f2961
languageName: node
linkType: hard
"joi@npm:^17.7.0":
version: 17.10.2
resolution: "joi@npm:17.10.2"
@ -28426,6 +28311,13 @@ __metadata:
languageName: node
linkType: hard
"luxon@npm:^3.4.4":
version: 3.4.4
resolution: "luxon@npm:3.4.4"
checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c
languageName: node
linkType: hard
"lz-string@npm:^1.4.4":
version: 1.4.4
resolution: "lz-string@npm:1.4.4"
@ -30877,13 +30769,6 @@ __metadata:
languageName: node
linkType: hard
"object-path@npm:^0.11.8":
version: 0.11.8
resolution: "object-path@npm:0.11.8"
checksum: 684ccf0fb6b82f067dc81e2763481606692b8485bec03eb2a64e086a44dbea122b2b9ef44423a08e09041348fe4b4b67bd59985598f1652f67df95f0618f5968
languageName: node
linkType: hard
"object-treeify@npm:^1.1.33":
version: 1.1.33
resolution: "object-treeify@npm:1.1.33"
@ -32826,24 +32711,6 @@ __metadata:
languageName: node
linkType: hard
"prisma-field-encryption@npm:^1.4.0":
version: 1.5.0
resolution: "prisma-field-encryption@npm:1.5.0"
dependencies:
"@47ng/cloak": ^1.1.0
"@prisma/generator-helper": ^5.0.0
debug: ^4.3.4
immer: ^10.0.2
object-path: ^0.11.8
zod: ^3.21.4
peerDependencies:
"@prisma/client": ">= 4.7"
bin:
prisma-field-encryption: dist/generator/main.js
checksum: 530bd970c5015c8c587bdca24136262d746093dd3d0793c892f4a1c377633182ae95e0ef5659465f914e4cc9bd7b24bf45551aa9c624a05942570f5b650fc065
languageName: node
linkType: hard
"prisma-kysely@npm:^1.7.1":
version: 1.7.1
resolution: "prisma-kysely@npm:1.7.1"
@ -33472,16 +33339,6 @@ __metadata:
languageName: node
linkType: hard
"react-chartjs-2@npm:^4.0.1":
version: 4.3.1
resolution: "react-chartjs-2@npm:4.3.1"
peerDependencies:
chart.js: ^3.5.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 574d12cc43b9b4a0f1e04cc692982e16ef7083c03da2a8a9fc2180fe9bcadc793008f81d8f4eec5465925eff84c95d7c523cb74376858b363ae75a83bb3c2a5d
languageName: node
linkType: hard
"react-colorful@npm:^5.1.2":
version: 5.6.1
resolution: "react-colorful@npm:5.6.1"
@ -35483,13 +35340,6 @@ __metadata:
languageName: node
linkType: hard
"s-ago@npm:^2.2.0":
version: 2.2.0
resolution: "s-ago@npm:2.2.0"
checksum: f665fef44d9d88322ce5a798ca3c49b40f96231ddc7bd46dc23c883e98215675aa422985760d45d3779faa3c0bc94edb2a50630bf15f54c239d11963e53d998c
languageName: node
linkType: hard
"sade@npm:^1.7.3":
version: 1.8.1
resolution: "sade@npm:1.8.1"
@ -37400,15 +37250,6 @@ __metadata:
languageName: node
linkType: hard
"swr@npm:^1.2.2":
version: 1.3.0
resolution: "swr@npm:1.3.0"
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
checksum: e7a184f0d560e9c8be85c023cc8e65e56a88a6ed46f9394b301b07f838edca23d2e303685319a4fcd620b81d447a7bcb489c7fa0a752c259f91764903c690cdb
languageName: node
linkType: hard
"symbol-observable@npm:4.0.0":
version: 4.0.0
resolution: "symbol-observable@npm:4.0.0"
@ -37504,39 +37345,6 @@ __metadata:
languageName: node
linkType: hard
"tailwindcss@npm:^3.3.1":
version: 3.3.6
resolution: "tailwindcss@npm:3.3.6"
dependencies:
"@alloc/quick-lru": ^5.2.0
arg: ^5.0.2
chokidar: ^3.5.3
didyoumean: ^1.2.2
dlv: ^1.1.3
fast-glob: ^3.3.0
glob-parent: ^6.0.2
is-glob: ^4.0.3
jiti: ^1.19.1
lilconfig: ^2.1.0
micromatch: ^4.0.5
normalize-path: ^3.0.0
object-hash: ^3.0.0
picocolors: ^1.0.0
postcss: ^8.4.23
postcss-import: ^15.1.0
postcss-js: ^4.0.1
postcss-load-config: ^4.0.1
postcss-nested: ^6.0.1
postcss-selector-parser: ^6.0.11
resolve: ^1.22.2
sucrase: ^3.32.0
bin:
tailwind: lib/cli.js
tailwindcss: lib/cli.js
checksum: 44632ac471248ecebcee1a2f15a0c3e9b8383513e71692b586aa2fe56dca12828ff70de3d340c898f27b27480e8475e5eb345fb2ebb813028bb2393578a34337
languageName: node
linkType: hard
"tailwindcss@npm:^3.3.3":
version: 3.3.3
resolution: "tailwindcss@npm:3.3.3"