chore(platform): OAuth Flow (#12798)
This commit is contained in:
parent
b987f6ea4d
commit
0830f3304e
|
@ -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"],
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { IsString } from "class-validator";
|
||||
|
||||
export class OAuthAuthorizeInput {
|
||||
@IsString()
|
||||
redirect_uri!: string;
|
||||
|
||||
@IsString()
|
||||
client_id!: string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { IsString } from "class-validator";
|
||||
|
||||
export class ExchangeAuthorizationCodeInput {
|
||||
@IsString()
|
||||
client_id!: string;
|
||||
|
||||
@IsString()
|
||||
client_secret!: string;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IsString } from "class-validator";
|
||||
|
||||
export class RefreshTokenInput {
|
||||
@IsString()
|
||||
refresh_token!: string;
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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 {}
|
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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
242
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user