Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5a1222fc5 | ||
|
|
2b892296b0 | ||
|
|
f796802ed3 | ||
|
|
b2ac227877 | ||
|
|
f0aec4efdc | ||
|
|
8d7cdf9d3b | ||
|
|
2ed0380e4f | ||
|
|
752cfa6bf9 | ||
|
|
f11c4dd7ec | ||
|
|
386f60ca6d | ||
|
|
e98f279f64 | ||
|
|
fe190f20f3 | ||
|
|
96855ea391 | ||
|
|
57150d992e | ||
|
|
8b75bf347f | ||
|
|
788fbdbe5e | ||
|
|
b2239374bc | ||
|
|
77c61abef8 | ||
|
|
c2472cc05a | ||
|
|
608fe1c586 | ||
|
|
4413295b27 | ||
|
|
bcc433ba9a | ||
|
|
2ba571d230 | ||
|
|
4a0a1ef053 | ||
|
|
7e882eedf8 | ||
|
|
224e606439 | ||
|
|
14b3344a41 | ||
|
|
a538ba64d2 | ||
|
|
b3bfad7fea | ||
|
|
f80b45e36a | ||
|
|
f3d60b141f | ||
|
|
5463dc261c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
Reduced bundle size by removing unnecessary types
|
||||
@@ -1,9 +0,0 @@
|
||||
node_modules
|
||||
apps/api/v2/dist
|
||||
packages/platform/**/dist/*
|
||||
**/**/node_modules
|
||||
**/**/.next
|
||||
**/**/public
|
||||
packages/prisma/zod
|
||||
apps/web/public/embed
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["./packages/config/eslint-preset.js"],
|
||||
plugins: ["import"],
|
||||
rules: {
|
||||
"import/no-cycle": ["warn", { maxDepth: Infinity }],
|
||||
},
|
||||
overrides: [
|
||||
// WARN: features must not be imported by app-store or lib
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/app-store or @calcom/lib.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// WARN: lib must not import app-store or features
|
||||
{
|
||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into app-store
|
||||
"**/app-store",
|
||||
"**/app-store/*",
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch alias imports
|
||||
"@calcom/app-store",
|
||||
"@calcom/app-store/*",
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "@calcom/lib should not import @calcom/app-store or @calcom/features.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// ERROR: app-store must not import trpc
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into trpc
|
||||
"**/trpc",
|
||||
"**/trpc/*",
|
||||
// Catch alias imports
|
||||
"@calcom/trpc",
|
||||
"@calcom/trpc/*",
|
||||
"@trpc",
|
||||
"@trpc/*",
|
||||
],
|
||||
message:
|
||||
"@calcom/app-store must not import trpc. Move UI to apps/web/components/apps or introduce an API boundary.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ERROR: prisma must not import `features` package
|
||||
{
|
||||
files: ["packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/prisma.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
name: E2E
|
||||
name: Check breaking changes and run E2E
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
@@ -23,6 +23,10 @@ env:
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_VAPID_PUBLIC_KEY }}
|
||||
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
|
||||
JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
|
||||
NODE_ENV: ${{ vars.CI_NODE_ENV }}
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
@@ -68,6 +72,18 @@ jobs:
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
|
||||
- name: Generate Swagger
|
||||
working-directory: apps/api/v2
|
||||
run: yarn generate-swagger
|
||||
|
||||
- name: Check breaking changes
|
||||
uses: oasdiff/oasdiff-action/breaking@main
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json
|
||||
revision: docs/api-reference/v2/openapi.json
|
||||
fail-on: WARN
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
@@ -75,6 +91,7 @@ jobs:
|
||||
EXIT_CODE=$?
|
||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -16,5 +16,4 @@ public
|
||||
packages/prisma/zod
|
||||
packages/prisma/enums
|
||||
apps/web/public/embed
|
||||
apps/api/v2/swagger/documentation.json
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
Vendored
Vendored
Vendored
@@ -13,5 +13,8 @@
|
||||
"connect": "^3.7.0",
|
||||
"http": "^0.0.1-security",
|
||||
"http-proxy-middleware": "^2.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/eslint-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default nextJsConfig;
|
||||
+11
-10
@@ -10,7 +10,7 @@
|
||||
"build": "next build",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||
"dev": "PORT=3003 next dev",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"start": "PORT=3003 next start",
|
||||
"docker-start-api": "PORT=80 next start",
|
||||
@@ -18,18 +18,19 @@
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/types": "*",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@calcom/tsconfig": "workspace:*",
|
||||
"@calcom/types": "workspace:*",
|
||||
"node-mocks-http": "^1.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/emails": "*",
|
||||
"@calcom/features": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/app-store": "workspace:*",
|
||||
"@calcom/dayjs": "workspace:*",
|
||||
"@calcom/emails": "workspace:*",
|
||||
"@calcom/features": "workspace:*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/trpc": "workspace:*",
|
||||
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
||||
"@sentry/nextjs": "^9.15.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint/eslint-plugin"],
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js", "next-i18next.config.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["./src/**/*.controller.ts"],
|
||||
excludedFiles: "*.spec.js",
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
|
||||
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
|
||||
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
|
||||
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
|
||||
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
@@ -30,18 +30,19 @@
|
||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.ts --watch",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts",
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register src/swagger/copy-swagger-module.ts",
|
||||
"generate-swagger": "yarn copy-swagger-module && yarn build && node ./dist/apps/api/v2/src/swagger/generate-swagger-script.js",
|
||||
"prepare": "yarn run snyk-protect",
|
||||
"snyk-protect": "snyk-protect"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "*",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/platform-constants": "workspace:*",
|
||||
"@calcom/platform-enums": "workspace:*",
|
||||
"@calcom/platform-libraries": "workspace:*",
|
||||
"@calcom/platform-types": "workspace:*",
|
||||
"@calcom/platform-utils": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@golevelup/ts-jest": "^0.4.0",
|
||||
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
||||
"@nest-lab/throttler-storage-redis": "1.0.0",
|
||||
@@ -88,6 +89,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(ReassignBookingOutputData_2024_08_13)
|
||||
export class ReassignBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
|
||||
+12
-108
@@ -1,39 +1,29 @@
|
||||
import type { AppConfig } from "@/config/type";
|
||||
import { getEnv } from "@/env";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
import {
|
||||
PathItemObject,
|
||||
PathsObject,
|
||||
OperationObject,
|
||||
TagObject,
|
||||
} from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
|
||||
import "dotenv/config";
|
||||
import * as fs from "fs";
|
||||
import { Server } from "http";
|
||||
import { WinstonModule } from "nest-winston";
|
||||
|
||||
import { bootstrap } from "./app";
|
||||
import { AppModule } from "./app.module";
|
||||
import { loggerConfig } from "./lib/logger";
|
||||
import { generateSwaggerForApp } from "./swagger/generate-swagger";
|
||||
|
||||
const HttpMethods: (keyof PathItemObject)[] = ["get", "post", "put", "delete", "patch", "options", "head"];
|
||||
|
||||
const run = async () => {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: WinstonModule.createLogger(loggerConfig()),
|
||||
bodyParser: false,
|
||||
});
|
||||
run().catch((error: Error) => {
|
||||
console.error("Failed to start Cal Platform API", { error: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function run() {
|
||||
const app = await createNestApp();
|
||||
const logger = new Logger("App");
|
||||
|
||||
try {
|
||||
bootstrap(app);
|
||||
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
|
||||
void generateSwagger(app);
|
||||
generateSwaggerForApp(app);
|
||||
await app.listen(port);
|
||||
logger.log(`Application started on port: ${port}`);
|
||||
} catch (error) {
|
||||
@@ -42,97 +32,11 @@ const run = async () => {
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function customTagSort(a: string, b: string): number {
|
||||
const platformPrefix = "Platform";
|
||||
const orgsPrefix = "Orgs";
|
||||
|
||||
if (a.startsWith(platformPrefix) && !b.startsWith(platformPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(platformPrefix) && b.startsWith(platformPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.startsWith(orgsPrefix) && !b.startsWith(orgsPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(orgsPrefix) && b.startsWith(orgsPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
function isOperationObject(obj: any): obj is OperationObject {
|
||||
return obj && typeof obj === "object" && "tags" in obj;
|
||||
}
|
||||
|
||||
function groupAndSortPathsByFirstTag(paths: PathsObject): PathsObject {
|
||||
const groupedPaths: { [key: string]: PathsObject } = {};
|
||||
|
||||
Object.keys(paths).forEach((pathKey) => {
|
||||
const pathItem = paths[pathKey];
|
||||
|
||||
HttpMethods.forEach((method) => {
|
||||
const operation = pathItem[method];
|
||||
|
||||
if (isOperationObject(operation) && operation.tags && operation.tags.length > 0) {
|
||||
const firstTag = operation.tags[0];
|
||||
|
||||
if (!groupedPaths[firstTag]) {
|
||||
groupedPaths[firstTag] = {};
|
||||
}
|
||||
|
||||
groupedPaths[firstTag][pathKey] = pathItem;
|
||||
}
|
||||
});
|
||||
export async function createNestApp() {
|
||||
return NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: WinstonModule.createLogger(loggerConfig()),
|
||||
bodyParser: false,
|
||||
});
|
||||
|
||||
const sortedTags = Object.keys(groupedPaths).sort(customTagSort);
|
||||
const sortedPaths: PathsObject = {};
|
||||
|
||||
sortedTags.forEach((tag) => {
|
||||
Object.assign(sortedPaths, groupedPaths[tag]);
|
||||
});
|
||||
|
||||
return sortedPaths;
|
||||
}
|
||||
|
||||
async function generateSwagger(app: NestExpressApplication<Server>) {
|
||||
const logger = new Logger("App");
|
||||
logger.log(`Generating Swagger documentation...\n`);
|
||||
|
||||
const config = new DocumentBuilder().setTitle("Cal.com API v2").build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
document.paths = groupAndSortPathsByFirstTag(document.paths);
|
||||
|
||||
const swaggerOutputFile = "./swagger/documentation.json";
|
||||
const docsOutputFile = "../../../docs/api-reference/v2/openapi.json";
|
||||
const stringifiedContents = JSON.stringify(document, null, 2);
|
||||
|
||||
if (fs.existsSync(swaggerOutputFile)) {
|
||||
fs.unlinkSync(swaggerOutputFile);
|
||||
}
|
||||
|
||||
fs.writeFileSync(swaggerOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
|
||||
if (fs.existsSync(docsOutputFile) && getEnv("NODE_ENV") === "development") {
|
||||
fs.unlinkSync(docsOutputFile);
|
||||
fs.writeFileSync(docsOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
}
|
||||
|
||||
if (!process.env.DOCS_URL) {
|
||||
SwaggerModule.setup("docs", app, document, {
|
||||
customCss: ".swagger-ui .topbar { display: none }",
|
||||
});
|
||||
|
||||
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error: Error) => {
|
||||
console.error("Failed to start Cal Platform API", { error: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import {
|
||||
NO_AUTH_PROVIDED_MESSAGE,
|
||||
ONLY_CLIENT_ID_PROVIDED_MESSAGE,
|
||||
ONLY_CLIENT_SECRET_PROVIDED_MESSAGE,
|
||||
} from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
|
||||
export class OptionalApiAuthGuard extends ApiAuthGuard {
|
||||
@@ -9,11 +10,16 @@ export class OptionalApiAuthGuard extends ApiAuthGuard {
|
||||
// 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);
|
||||
const onlyClientSecretProvided = error && error.message.includes(ONLY_CLIENT_SECRET_PROVIDED_MESSAGE);
|
||||
|
||||
if (onlyClientIdProvided) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (onlyClientSecretProvided) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user || noAuthProvided || !error) {
|
||||
return user || null;
|
||||
} else {
|
||||
|
||||
@@ -32,7 +32,11 @@ import { randomString } from "test/utils/randomString";
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
|
||||
|
||||
import { ApiAuthGuardRequest, ONLY_CLIENT_ID_PROVIDED_MESSAGE } from "./api-auth.strategy";
|
||||
import {
|
||||
ApiAuthGuardRequest,
|
||||
ONLY_CLIENT_ID_PROVIDED_MESSAGE,
|
||||
ONLY_CLIENT_SECRET_PROVIDED_MESSAGE,
|
||||
} from "./api-auth.strategy";
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
@@ -267,6 +271,29 @@ describe("ApiAuthStrategy", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if only OAuth Client Secret is provided", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
[X_CAL_SECRET_KEY]: `${oAuthClient.secret}gibberish`,
|
||||
},
|
||||
get: (key: string) => ({ origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
try {
|
||||
await strategy.authenticate(request);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
expect(error.getStatus()).toEqual(401);
|
||||
expect(error.message).toContain(ONLY_CLIENT_SECRET_PROVIDED_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if OAuth ID is invalid", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
|
||||
@@ -29,11 +29,11 @@ export type ApiAuthGuardRequest = Request & {
|
||||
user: ApiAuthGuardUser;
|
||||
allowedAuthMethods?: AllowedAuthMethod[];
|
||||
};
|
||||
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 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";
|
||||
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`;
|
||||
|
||||
export const ONLY_CLIENT_SECRET_PROVIDED_MESSAGE = `Only '${X_CAL_SECRET_KEY}' header provided. Please also provide '${X_CAL_CLIENT_ID}' header or Auth bearer token as 'Authentication' header`;
|
||||
|
||||
@Injectable()
|
||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||
@@ -121,6 +121,9 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||
const onlyClientIdProvided = !!oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||
const onlyClientSecretProvided =
|
||||
!oAuthClientId && !!oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||
|
||||
if (noAuthProvided) {
|
||||
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
||||
}
|
||||
@@ -129,6 +132,10 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_ID_PROVIDED_MESSAGE}`);
|
||||
}
|
||||
|
||||
if (onlyClientSecretProvided) {
|
||||
throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_SECRET_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"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
|
||||
import { BillingProcessor } from "@/modules/billing/billing.processor";
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
||||
import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service";
|
||||
@@ -9,6 +10,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { BullModule } from "@nestjs/bull";
|
||||
@@ -18,6 +20,7 @@ import { Module } from "@nestjs/common";
|
||||
imports: [
|
||||
PrismaModule,
|
||||
StripeModule,
|
||||
RedisModule,
|
||||
MembershipsModule,
|
||||
OrganizationsModule,
|
||||
BullModule.registerQueue({
|
||||
@@ -32,6 +35,10 @@ import { Module } from "@nestjs/common";
|
||||
providers: [
|
||||
BillingConfigService,
|
||||
BillingService,
|
||||
{
|
||||
provide: "IBillingService",
|
||||
useClass: BillingServiceCachingProxy,
|
||||
},
|
||||
BillingRepository,
|
||||
BillingProcessor,
|
||||
ManagedOrganizationsBillingService,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
@@ -16,7 +17,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
import { withNextAuth } from "test/utils/withNextAuth";
|
||||
|
||||
import type { Team, PlatformBilling } from "@calcom/prisma/client";
|
||||
|
||||
@@ -31,9 +32,8 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
let organization: Team;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
const moduleRef = await withNextAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
@@ -44,8 +44,10 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({
|
||||
name: `billing-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
});
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
@@ -87,6 +89,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/billing/webhook (GET) should not get billing plan for org since it's not set yet", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/billing/${organization.id}/check`)
|
||||
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const data = res.body.data as CheckPlatformBillingResponseDto;
|
||||
expect(data?.plan).toEqual("FREE");
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (POST) should set billing free plan for org", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
@@ -119,6 +132,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
expect(billing?.plan).toEqual("FREE");
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (GET) should get billing plan for org", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/billing/${organization.id}/check`)
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const data = res.body.data as CheckPlatformBillingResponseDto;
|
||||
expect(data?.plan).toEqual("FREE");
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles
|
||||
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
|
||||
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import {
|
||||
Body,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Headers,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Logger,
|
||||
Delete,
|
||||
ParseIntPipe,
|
||||
@@ -40,7 +41,7 @@ export class BillingController {
|
||||
private logger = new Logger("Billing Controller");
|
||||
|
||||
constructor(
|
||||
private readonly billingService: BillingService,
|
||||
@Inject("IBillingService") private readonly billingService: IBillingService,
|
||||
public readonly stripeService: StripeService,
|
||||
private readonly configService: ConfigService<AppConfig>
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import type { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import Stripe from "stripe";
|
||||
import { PlatformBilling, Team } from "@calcom/prisma/client";
|
||||
|
||||
export type BillingData = {
|
||||
team: (Team & { platformBilling: PlatformBilling | null }) | null;
|
||||
status: "valid" | "no_subscription" | "no_billing";
|
||||
plan: string;
|
||||
};
|
||||
|
||||
export interface IBillingService {
|
||||
getBillingData(teamId: number): Promise<BillingData>;
|
||||
createTeamBilling(teamId: number): Promise<string>;
|
||||
redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string): Promise<string>;
|
||||
updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise<string>;
|
||||
cancelTeamSubscription(teamId: number): Promise<void>;
|
||||
handleStripeSubscriptionDeleted(event: Stripe.Event): Promise<void>;
|
||||
handleStripePaymentSuccess(event: Stripe.Event): Promise<void>;
|
||||
handleStripePaymentFailed(event: Stripe.Event): Promise<void>;
|
||||
handleStripePaymentPastDue(event: Stripe.Event): Promise<void>;
|
||||
handleStripeCheckoutEvents(event: Stripe.Event): Promise<void>;
|
||||
handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise<void>;
|
||||
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null;
|
||||
getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null;
|
||||
stripeService: StripeService;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { IBillingService, BillingData } from "@/modules/billing/interfaces/billing-service.interface";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const REDIS_BILLING_CACHE_KEY = (teamId: number) => `apiv2:team:${teamId}:billing`;
|
||||
export const BILLING_CACHE_TTL_MS = 3_600_000; // 1 hour
|
||||
|
||||
@Injectable()
|
||||
export class BillingServiceCachingProxy implements IBillingService {
|
||||
constructor(private readonly billingService: BillingService, private readonly redisService: RedisService) {}
|
||||
|
||||
async getBillingData(teamId: number) {
|
||||
const cachedBillingData = await this.getBillingCache(teamId);
|
||||
if (cachedBillingData) {
|
||||
return cachedBillingData;
|
||||
}
|
||||
|
||||
const billingData = await this.billingService.getBillingData(teamId);
|
||||
await this.setBillingCache(teamId, billingData);
|
||||
return billingData;
|
||||
}
|
||||
|
||||
private async deleteBillingCache(teamId: number) {
|
||||
await this.redisService.del(REDIS_BILLING_CACHE_KEY(teamId));
|
||||
}
|
||||
|
||||
private async getBillingCache(teamId: number) {
|
||||
const cachedResult = await this.redisService.get<BillingData>(REDIS_BILLING_CACHE_KEY(teamId));
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
private async setBillingCache(teamId: number, billingData: BillingData): Promise<void> {
|
||||
await this.redisService.set<BillingData>(REDIS_BILLING_CACHE_KEY(teamId), billingData, {
|
||||
ttl: BILLING_CACHE_TTL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async createTeamBilling(teamId: number): Promise<string> {
|
||||
return this.billingService.createTeamBilling(teamId);
|
||||
}
|
||||
|
||||
async redirectToSubscribeCheckout(
|
||||
teamId: number,
|
||||
plan: PlatformPlan,
|
||||
customerId?: string
|
||||
): Promise<string> {
|
||||
return this.billingService.redirectToSubscribeCheckout(teamId, plan, customerId);
|
||||
}
|
||||
|
||||
async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise<string> {
|
||||
return this.billingService.updateSubscriptionForTeam(teamId, plan);
|
||||
}
|
||||
|
||||
async cancelTeamSubscription(teamId: number): Promise<void> {
|
||||
await this.billingService.cancelTeamSubscription(teamId);
|
||||
await this.deleteBillingCache(teamId);
|
||||
}
|
||||
|
||||
async handleStripeSubscriptionDeleted(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripeSubscriptionDeleted(event);
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const teamId = subscription?.metadata?.teamId;
|
||||
if (teamId) {
|
||||
await this.deleteBillingCache(Number.parseInt(teamId));
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentSuccess(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripePaymentSuccess(event);
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||
if (subscriptionId) {
|
||||
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||
subscriptionId
|
||||
);
|
||||
if (teamBilling?.id) {
|
||||
await this.deleteBillingCache(teamBilling.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentFailed(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripePaymentFailed(event);
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||
if (subscriptionId) {
|
||||
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||
subscriptionId
|
||||
);
|
||||
if (teamBilling?.id) {
|
||||
await this.deleteBillingCache(teamBilling.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentPastDue(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripePaymentPastDue(event);
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const subscriptionId = subscription.id;
|
||||
if (subscriptionId) {
|
||||
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||
subscriptionId
|
||||
);
|
||||
if (teamBilling?.id) {
|
||||
await this.deleteBillingCache(teamBilling.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripeCheckoutEvents(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripeCheckoutEvents(event);
|
||||
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||
const teamId = checkoutSession.metadata?.teamId;
|
||||
if (teamId) {
|
||||
await this.deleteBillingCache(Number.parseInt(teamId));
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise<void> {
|
||||
return this.billingService.handleStripeSubscriptionForActiveManagedUsers(event);
|
||||
}
|
||||
|
||||
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||
return this.billingService.getSubscriptionIdFromInvoice(invoice);
|
||||
}
|
||||
|
||||
getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||
return this.billingService.getCustomerIdFromInvoice(invoice);
|
||||
}
|
||||
|
||||
get stripeService() {
|
||||
return this.billingService.stripeService;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { AppConfig } from "@/config/type";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
@@ -23,14 +24,14 @@ import { DateTime } from "luxon";
|
||||
import Stripe from "stripe";
|
||||
|
||||
@Injectable()
|
||||
export class BillingService implements OnModuleDestroy {
|
||||
export class BillingService implements IBillingService, OnModuleDestroy {
|
||||
private logger = new Logger("BillingService");
|
||||
private readonly webAppUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly teamsRepository: OrganizationsRepository,
|
||||
public readonly stripeService: StripeService,
|
||||
private readonly billingRepository: BillingRepository,
|
||||
public readonly billingRepository: BillingRepository,
|
||||
private readonly configService: ConfigService<AppConfig>,
|
||||
private readonly billingConfigService: BillingConfigService,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
@@ -43,18 +44,23 @@ export class BillingService implements OnModuleDestroy {
|
||||
|
||||
async getBillingData(teamId: number) {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
|
||||
if (teamWithBilling?.platformBilling) {
|
||||
if (!teamWithBilling?.platformBilling.subscriptionId) {
|
||||
return { team: teamWithBilling, status: "no_subscription", plan: "none" };
|
||||
return { team: teamWithBilling, status: "no_subscription" as const, plan: "none" };
|
||||
} else {
|
||||
return {
|
||||
team: teamWithBilling,
|
||||
status: "valid" as const,
|
||||
plan: teamWithBilling.platformBilling.plan,
|
||||
};
|
||||
}
|
||||
|
||||
return { team: teamWithBilling, status: "valid", plan: teamWithBilling.platformBilling.plan };
|
||||
} else {
|
||||
return { team: teamWithBilling, status: "no_billing", plan: "none" };
|
||||
return { team: teamWithBilling, status: "no_billing" as const, plan: "none" };
|
||||
}
|
||||
}
|
||||
|
||||
async createTeamBilling(teamId: number) {
|
||||
async createTeamBilling(teamId: number): Promise<string> {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
let customerId = teamWithBilling?.platformBilling?.customerId;
|
||||
|
||||
@@ -67,7 +73,7 @@ export class BillingService implements OnModuleDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
return customerId;
|
||||
return customerId!;
|
||||
}
|
||||
|
||||
async redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string) {
|
||||
|
||||
+7
-1
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsISO8601, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -297,6 +297,12 @@ export class DateTimeWithZone {
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(
|
||||
CalendarEventVideoLocation,
|
||||
CalendarEventPhoneLocation,
|
||||
CalendarEventSipLocation,
|
||||
CalendarEventMoreLocation
|
||||
)
|
||||
export class UnifiedCalendarEventOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => DateTimeWithZone)
|
||||
|
||||
+3
-1
@@ -4,9 +4,11 @@ import {
|
||||
MicrosoftServiceAccountKeyInput,
|
||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, IsNotEmpty, ValidateNested, Validate } from "class-validator";
|
||||
import { IsString, IsNotEmpty, Validate } from "class-validator";
|
||||
|
||||
@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput)
|
||||
export class CreateDelegationCredentialInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
+4
-3
@@ -3,10 +3,11 @@ import {
|
||||
MicrosoftServiceAccountKeyInput,
|
||||
ServiceAccountKeyValidator,
|
||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||
import { ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose, plainToClass, Transform, Type } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, Validate, ValidateNested } from "class-validator";
|
||||
import { ApiExtraModels, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, Validate } from "class-validator";
|
||||
|
||||
@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput)
|
||||
export class UpdateDelegationCredentialInput {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
@@ -174,4 +174,14 @@ export class OrganizationsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findTeamByPlatformBillingId(billingId: number) {
|
||||
return this.dbRead.prisma.team.findFirst({
|
||||
where: {
|
||||
platformBilling: {
|
||||
id: billingId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -75,6 +75,7 @@ class Routing {
|
||||
crmOwnerRecordType?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04)
|
||||
export class CreateRoutingFormResponseOutputData {
|
||||
@ApiPropertyOptional({
|
||||
type: Number,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||
export class CreateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||
export class UpdateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
class Output {
|
||||
class CreateTeamOutputData {
|
||||
@Expose()
|
||||
@IsString()
|
||||
message!: string;
|
||||
@@ -19,16 +19,17 @@ class Output {
|
||||
pendingTeam!: TeamOutputDto;
|
||||
}
|
||||
|
||||
@ApiExtraModels(TeamOutputDto, CreateTeamOutputData)
|
||||
export class CreateTeamOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
oneOf: [{ $ref: getSchemaPath(Output) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
||||
oneOf: [{ $ref: getSchemaPath(CreateTeamOutputData) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
||||
description: "Either an Output object or a TeamOutputDto.",
|
||||
})
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
data!: Output | TeamOutputDto;
|
||||
data!: CreateTeamOutputData | TeamOutputDto;
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import * as path from "path";
|
||||
// "nest-cli" with the "nest-cli.json" file, and for nest cli to be loaded with plugins correctly the "@nestjs/swagger"
|
||||
// should reside in the project's node_modules already before the "nest start" command is executed.
|
||||
async function copyNestSwagger() {
|
||||
const monorepoRoot = path.resolve(__dirname, "../../../../");
|
||||
const nodeModulesNestjs = path.resolve(__dirname, "../node_modules/@nestjs");
|
||||
const monorepoRoot = path.resolve(__dirname, "../../../../../");
|
||||
const nodeModulesNestjs = path.resolve(__dirname, "../../node_modules/@nestjs");
|
||||
const swaggerModulePath = "@nestjs/swagger";
|
||||
|
||||
const sourceDir = path.join(monorepoRoot, "node_modules", swaggerModulePath);
|
||||
@@ -0,0 +1,29 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { bootstrap } from "../app";
|
||||
import { createNestApp } from "../main";
|
||||
import { generateSwaggerForApp } from "../swagger/generate-swagger";
|
||||
|
||||
generateSwagger()
|
||||
.then(() => {
|
||||
console.log("✅ Swagger generation completed successfully");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error("❌ Failed to generate swagger", { error: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function generateSwagger() {
|
||||
const app = await createNestApp();
|
||||
|
||||
try {
|
||||
bootstrap(app);
|
||||
await generateSwaggerForApp(app);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
import {
|
||||
PathItemObject,
|
||||
PathsObject,
|
||||
OperationObject,
|
||||
} from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
|
||||
import "dotenv/config";
|
||||
import * as fs from "fs";
|
||||
import { Server } from "http";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const HttpMethods: (keyof PathItemObject)[] = ["get", "post", "put", "delete", "patch", "options", "head"];
|
||||
|
||||
export async function generateSwaggerForApp(app: NestExpressApplication<Server>) {
|
||||
const logger = new Logger("App");
|
||||
logger.log(`Generating Swagger documentation...\n`);
|
||||
|
||||
const config = new DocumentBuilder().setTitle("Cal.com API v2").build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
document.paths = groupAndSortPathsByFirstTag(document.paths);
|
||||
|
||||
const docsOutputFile = "../../../docs/api-reference/v2/openapi.json";
|
||||
const stringifiedContents = JSON.stringify(document, null, 2);
|
||||
|
||||
if (fs.existsSync(docsOutputFile) && getEnv("NODE_ENV") === "development") {
|
||||
fs.unlinkSync(docsOutputFile);
|
||||
fs.writeFileSync(docsOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
spawnSync("npx", ["prettier", docsOutputFile, "--write"], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
if (!process.env.DOCS_URL) {
|
||||
SwaggerModule.setup("docs", app, document, {
|
||||
customCss: ".swagger-ui .topbar { display: none }",
|
||||
});
|
||||
|
||||
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function groupAndSortPathsByFirstTag(paths: PathsObject): PathsObject {
|
||||
const groupedPaths: { [key: string]: PathsObject } = {};
|
||||
|
||||
Object.keys(paths).forEach((pathKey) => {
|
||||
const pathItem = paths[pathKey];
|
||||
|
||||
HttpMethods.forEach((method) => {
|
||||
const operation = pathItem[method];
|
||||
|
||||
if (isOperationObject(operation) && operation.tags && operation.tags.length > 0) {
|
||||
const firstTag = operation.tags[0];
|
||||
|
||||
if (!groupedPaths[firstTag]) {
|
||||
groupedPaths[firstTag] = {};
|
||||
}
|
||||
|
||||
groupedPaths[firstTag][pathKey] = pathItem;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sortedTags = Object.keys(groupedPaths).sort(customTagSort);
|
||||
const sortedPaths: PathsObject = {};
|
||||
|
||||
sortedTags.forEach((tag) => {
|
||||
Object.assign(sortedPaths, groupedPaths[tag]);
|
||||
});
|
||||
|
||||
return sortedPaths;
|
||||
}
|
||||
|
||||
function customTagSort(a: string, b: string): number {
|
||||
const platformPrefix = "Platform";
|
||||
const orgsPrefix = "Orgs";
|
||||
|
||||
if (a.startsWith(platformPrefix) && !b.startsWith(platformPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(platformPrefix) && b.startsWith(platformPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.startsWith(orgsPrefix) && !b.startsWith(orgsPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(orgsPrefix) && b.startsWith(orgsPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
function isOperationObject(obj: any): obj is OperationObject {
|
||||
return obj && typeof obj === "object" && "tags" in obj;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ export class PlatformBillingRepositoryFixture {
|
||||
id: orgId,
|
||||
customerId: `cus_123_${randomString}`,
|
||||
subscriptionId: `sub_123_${randomString}`,
|
||||
plan: plan || "STARTER",
|
||||
plan: plan || "FREE",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default nextJsConfig;
|
||||
@@ -6,12 +6,12 @@
|
||||
"dev": "next dev --turbopack --port=1337",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/config": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/config": "workspace:*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"airtable": "^0.12.2",
|
||||
"fumadocs-core": "^14.5.4",
|
||||
@@ -24,14 +24,12 @@
|
||||
"react-hot-toast": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@calcom/tsconfig": "workspace:*",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.9.0-beta"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
public/embed
|
||||
+25
-18
@@ -101,10 +101,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
|
||||
name: "privacy",
|
||||
href: "/settings/organizations/privacy",
|
||||
},
|
||||
{
|
||||
name: "billing",
|
||||
href: "/settings/organizations/billing",
|
||||
},
|
||||
|
||||
{ name: "OAuth Clients", href: "/settings/organizations/platform/oauth-clients" },
|
||||
{
|
||||
name: "SSO",
|
||||
@@ -173,17 +170,11 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
|
||||
// The following keys are assigned to admin only
|
||||
const adminRequiredKeys = ["admin"];
|
||||
const organizationRequiredKeys = ["organization"];
|
||||
const organizationAdminKeys = [
|
||||
"privacy",
|
||||
"billing",
|
||||
"OAuth Clients",
|
||||
"SSO",
|
||||
"directory_sync",
|
||||
"delegation_credential",
|
||||
];
|
||||
const organizationAdminKeys = ["privacy", "OAuth Clients", "SSO", "directory_sync", "delegation_credential"];
|
||||
|
||||
export interface SettingsPermissions {
|
||||
canViewRoles?: boolean;
|
||||
canViewOrganizationBilling?: boolean;
|
||||
}
|
||||
|
||||
const useTabs = ({
|
||||
@@ -232,11 +223,27 @@ const useTabs = ({
|
||||
|
||||
// Add pbac menu item only if feature flag is enabled AND user has permission to view roles
|
||||
// This prevents showing the menu item when user has no organization permissions
|
||||
if (isPbacEnabled && permissions?.canViewRoles) {
|
||||
newArray.push({
|
||||
name: "roles_and_permissions",
|
||||
href: "/settings/organizations/roles",
|
||||
});
|
||||
if (isPbacEnabled) {
|
||||
if (permissions?.canViewRoles) {
|
||||
newArray.push({
|
||||
name: "roles_and_permissions",
|
||||
href: "/settings/organizations/roles",
|
||||
});
|
||||
}
|
||||
|
||||
if (permissions?.canViewOrganizationBilling) {
|
||||
newArray.push({
|
||||
name: "billing",
|
||||
href: "/settings/organizations/billing",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (isOrgAdminOrOwner) {
|
||||
newArray.push({
|
||||
name: "billing",
|
||||
href: "/settings/organizations/billing",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -272,7 +279,7 @@ const useTabs = ({
|
||||
if (isAdmin) return true;
|
||||
return !adminRequiredKeys.includes(tab.name);
|
||||
});
|
||||
}, [isAdmin, orgBranding, isOrgAdminOrOwner, user, isDelegationCredentialEnabled]);
|
||||
}, [isAdmin, orgBranding, isOrgAdminOrOwner, user, isDelegationCredentialEnabled, isPbacEnabled, permissions]);
|
||||
|
||||
return processTabsMemod;
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ const Page = async () => {
|
||||
<SettingsHeader
|
||||
title={t("billing")}
|
||||
description={t("manage_billing_description")}
|
||||
borderInShellHeader={true}>
|
||||
borderInShellHeader={false}>
|
||||
<BillingView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,12 @@ import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { TeamFeatures } from "@calcom/features/flags/config";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
|
||||
import { Resource, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { Resource, CrudAction, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
@@ -45,13 +46,16 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
||||
|
||||
let teamFeatures: Record<number, TeamFeatures> | null = null;
|
||||
let canViewRoles = false;
|
||||
let canViewOrganizationBilling = false;
|
||||
const orgId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
|
||||
|
||||
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
|
||||
if (orgId) {
|
||||
const [features, rolePermissions] = await Promise.all([
|
||||
const isOrgAdminOrOwner = checkAdminOrOwner(session.user.org?.role);
|
||||
const [features, rolePermissions, organizationPermissions] = await Promise.all([
|
||||
getTeamFeatures(orgId),
|
||||
getCachedResourcePermissions(userId, orgId, Resource.Role),
|
||||
getCachedResourcePermissions(userId, orgId, Resource.Organization),
|
||||
]);
|
||||
|
||||
if (features) {
|
||||
@@ -62,6 +66,8 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
||||
// Check if user has permission to read roles
|
||||
const roleActions = PermissionMapper.toActionMap(rolePermissions, Resource.Role);
|
||||
canViewRoles = roleActions[CrudAction.Read] ?? false;
|
||||
const orgActions = PermissionMapper.toActionMap(organizationPermissions, Resource.Organization);
|
||||
canViewOrganizationBilling = orgActions[CustomAction.ManageBilling] ?? isOrgAdminOrOwner;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +76,7 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
||||
<SettingsLayoutAppDirClient
|
||||
{...props}
|
||||
teamFeatures={teamFeatures ?? {}}
|
||||
permissions={{ canViewRoles }}
|
||||
permissions={{ canViewRoles, canViewOrganizationBilling }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+6
-3
@@ -2,10 +2,11 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import BillingView from "~/settings/billing/billing-view";
|
||||
|
||||
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
|
||||
import { validateUserHasOrgPerms } from "../../actions/validateUserHasOrgPerms";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -18,9 +19,11 @@ export const generateMetadata = async () =>
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
await validateUserHasOrgAdmin();
|
||||
|
||||
// TODO(SEAN): Add PBAC to this page in the next PR
|
||||
await validateUserHasOrgPerms({
|
||||
permission: "organization.manageBilling",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
|
||||
-20
@@ -1,24 +1,4 @@
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
const OrgAdminOnlyLayout = async ({ children }: { children: React.ReactNode }) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const userProfile = session?.user?.profile;
|
||||
const userId = session?.user?.id;
|
||||
const orgRole =
|
||||
session?.user?.org?.role ??
|
||||
userProfile?.organization?.members.find((m: { userId: number }) => m.userId === userId)?.role;
|
||||
const isOrgAdminOrOwner = checkAdminOrOwner(orgRole);
|
||||
|
||||
if (!isOrgAdminOrOwner) {
|
||||
return redirect("/settings/organizations/profile");
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { validateUserHasOrg } from "./validateUserHasOrg";
|
||||
|
||||
export const validateUserHasOrgPerms = async ({
|
||||
redirectTo,
|
||||
fallbackRoles,
|
||||
permission,
|
||||
}: {
|
||||
redirectTo?: string;
|
||||
permission: PermissionString;
|
||||
fallbackRoles: MembershipRole[];
|
||||
}) => {
|
||||
const session = await validateUserHasOrg();
|
||||
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
|
||||
const hasPermission = await permissionCheckService.checkPermission({
|
||||
userId: session.user.id,
|
||||
teamId: session.user.org.id,
|
||||
permission,
|
||||
fallbackRoles,
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
redirect(redirectTo || "/settings/my-account/profile");
|
||||
}
|
||||
|
||||
return session;
|
||||
};
|
||||
@@ -3,13 +3,15 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { BillingPlanService } from "@calcom/features/ee/billing/domain/billing-plans";
|
||||
import type { Contact } from "@calcom/features/ee/support/lib/intercom/intercom";
|
||||
import { intercom } from "@calcom/features/ee/support/lib/intercom/intercom";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -43,6 +45,11 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const { user } = session;
|
||||
|
||||
const membershipRepository = new MembershipRepository(prisma);
|
||||
const memberships = await membershipRepository.findAllMembershipsByUserIdForBilling({ userId: user.id });
|
||||
const billingPlanService = new BillingPlanService();
|
||||
const plan = await billingPlanService.getUserPlanByMemberships(memberships);
|
||||
|
||||
if (!existingContact.data) {
|
||||
const additionalUserInfo = await new UserRepository(prisma).getUserStats({ userId: session.user.id });
|
||||
const sumOfTeamEventTypes = additionalUserInfo?.teams.reduce(
|
||||
@@ -72,6 +79,7 @@ export async function POST(req: NextRequest) {
|
||||
sum_of_teams: additionalUserInfo?._count?.teams,
|
||||
sum_of_event_types: additionalUserInfo?._count?.eventTypes,
|
||||
sum_of_team_event_types: sumOfTeamEventTypes,
|
||||
Plan: plan,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -185,6 +185,10 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
|
||||
const isBookingFromRoutingForm = isBookingReroutable(parsedBooking);
|
||||
|
||||
const userSeat = booking.seatsReferences.find((seat) => !!userEmail && seat.attendee?.email === userEmail);
|
||||
|
||||
const isAttendee = !!userSeat;
|
||||
|
||||
const paymentAppData = getPaymentAppData(booking.eventType);
|
||||
|
||||
const location = booking.location as ReturnType<typeof getEventLocationValue>;
|
||||
@@ -223,10 +227,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
};
|
||||
|
||||
const getSeatReferenceUid = () => {
|
||||
if (!booking.seatsReferences[0]) {
|
||||
return undefined;
|
||||
}
|
||||
return booking.seatsReferences[0].referenceUid;
|
||||
return userSeat?.referenceUid;
|
||||
};
|
||||
|
||||
const actionContext: BookingActionContext = {
|
||||
@@ -250,6 +251,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
booking.location === "integrations:daily" ||
|
||||
(typeof booking.location === "string" && booking.location.trim() === ""),
|
||||
showPendingPayment: paymentAppData.enabled && booking.payment.length && !booking.paid,
|
||||
isAttendee,
|
||||
cardCharged,
|
||||
attendeeList,
|
||||
getSeatReferenceUid,
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface BookingActionContext {
|
||||
isDisabledRescheduling: boolean;
|
||||
isCalVideoLocation: boolean;
|
||||
showPendingPayment: boolean;
|
||||
isAttendee: boolean;
|
||||
cardCharged: boolean;
|
||||
attendeeList: Array<{
|
||||
name: string;
|
||||
@@ -102,6 +103,7 @@ export function getEditEventActions(context: BookingActionContext): ActionType[]
|
||||
isDisabledRescheduling,
|
||||
isBookingFromRoutingForm,
|
||||
getSeatReferenceUid,
|
||||
isAttendee,
|
||||
t,
|
||||
} = context;
|
||||
|
||||
@@ -111,7 +113,7 @@ export function getEditEventActions(context: BookingActionContext): ActionType[]
|
||||
icon: "clock",
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
booking.seatsReferences.length && isAttendee ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
disabled:
|
||||
(isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default [...nextJsConfig];
|
||||
@@ -5,8 +5,8 @@ import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { determineReschedulePreventionRedirect } from "@calcom/features/bookings/lib/reschedule/determineReschedulePreventionRedirect";
|
||||
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
|
||||
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
@@ -39,7 +39,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
uid,
|
||||
seatReferenceUid: maybeSeatReferenceUid,
|
||||
bookingSeat,
|
||||
} = await maybeGetBookingUidFromSeat(prisma, bookingUid);
|
||||
} = await maybeGetBookingUidFromSeat(prisma, seatReferenceUid ? seatReferenceUid : bookingUid);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -30,16 +30,18 @@ declare global {
|
||||
export default function PlatformBillingUpgrade() {
|
||||
const pathname = usePathname();
|
||||
const { t } = useLocale();
|
||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
||||
useGetUserAttributes();
|
||||
|
||||
const returnTo = pathname;
|
||||
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
|
||||
const teamId = `teamId=${userOrgId}`;
|
||||
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}&${teamId}`;
|
||||
|
||||
const onContactSupportClick = async () => {
|
||||
if (window.Support) {
|
||||
window.Support.open();
|
||||
}
|
||||
};
|
||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
||||
useGetUserAttributes();
|
||||
|
||||
const { mutateAsync: removeTeamSubscription, isPending: isRemoveTeamSubscriptionLoading } =
|
||||
useUnsubscribeTeamToStripe({
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { MemberInvitationModalWithoutMembers } from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
||||
import MemberList from "@calcom/features/ee/teams/components/MemberList";
|
||||
@@ -23,18 +22,23 @@ interface TeamMembersViewProps {
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
attributes?: any[];
|
||||
attributes?: {
|
||||
id: string;
|
||||
name: string;
|
||||
options: {
|
||||
value: string;
|
||||
}[];
|
||||
}[];
|
||||
permissions: MemberPermissions;
|
||||
}
|
||||
|
||||
export const TeamMembersView = ({ team, facetedTeamValues, permissions }: TeamMembersViewProps) => {
|
||||
const { t } = useLocale();
|
||||
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
|
||||
const [showInviteLinkSettingsModal, setShowInviteLinkSettingsModal] = useState(false);
|
||||
const [_showInviteLinkSettingsModal, setShowInviteLinkSettingsModal] = useState(false);
|
||||
|
||||
// Use PBAC permissions if available, otherwise fall back to role-based check
|
||||
const isTeamAdminOrOwner = checkAdminOrOwner(team.membership.role);
|
||||
const canLoggedInUserSeeMembers = permissions?.canListMembers ?? (!team.isPrivate || isTeamAdminOrOwner);
|
||||
// Use PBAC permissions - server-side permission check should be done in parent component
|
||||
const canLoggedInUserSeeMembers = permissions?.canListMembers ?? false;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
|
||||
+17
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.7.2",
|
||||
"version": "5.7.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -20,8 +20,8 @@
|
||||
"copy-app-store-static": "node scripts/copy-app-store-static.js",
|
||||
"build": "turbo run copy-app-store-static && next build && yarn sentry:release",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/web.json",
|
||||
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts",
|
||||
"translate-locales": "ts-node scripts/check-missing-translations.ts",
|
||||
@@ -32,20 +32,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@boxyhq/saml-jackson": "1.52.2",
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/app-store-cli": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/app-store": "workspace:*",
|
||||
"@calcom/app-store-cli": "workspace:*",
|
||||
"@calcom/dayjs": "workspace:*",
|
||||
"@calcom/embed-core": "workspace:*",
|
||||
"@calcom/embed-react": "workspace:*",
|
||||
"@calcom/embed-snippet": "workspace:*",
|
||||
"@calcom/features": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/features": "workspace:*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/platform-enums": "workspace:*",
|
||||
"@calcom/platform-types": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/trpc": "workspace:*",
|
||||
"@calcom/tsconfig": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"@daily-co/daily-js": "^0.83.1",
|
||||
"@daily-co/daily-react": "^0.23.2",
|
||||
"@dub/analytics": "^0.0.27",
|
||||
@@ -158,8 +158,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@calcom/config": "*",
|
||||
"@calcom/types": "*",
|
||||
"@calcom/config": "workspace:*",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@calcom/types": "workspace:*",
|
||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||
"@playwright/test": "^1.45.3",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
|
||||
@@ -458,5 +458,128 @@ test.describe("Reschedule for booking with seats", () => {
|
||||
await expect(page.locator('[data-testid="confirm-reschedule-button"]')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Host reschedule from /upcoming page should have rescheduleUid parameter set to bookingUid", async ({
|
||||
page,
|
||||
users,
|
||||
bookings,
|
||||
}) => {
|
||||
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
||||
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
]);
|
||||
await user.apiLogin();
|
||||
|
||||
const bookingAttendees = await prisma.attendee.findMany({
|
||||
where: { bookingId: booking.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const bookingSeats = bookingAttendees.map((attendee) => ({
|
||||
bookingId: booking.id,
|
||||
attendeeId: attendee.id,
|
||||
referenceUid: uuidv4(),
|
||||
data: {
|
||||
responses: {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await prisma.bookingSeat.createMany({
|
||||
data: bookingSeats,
|
||||
});
|
||||
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.waitForSelector('[data-testid="bookings"]');
|
||||
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
await page.locator('[data-testid="reschedule"]').click();
|
||||
|
||||
await page.waitForURL((url) => {
|
||||
const rescheduleUid = url.searchParams.get("rescheduleUid");
|
||||
return !!rescheduleUid && rescheduleUid === booking.uid;
|
||||
});
|
||||
});
|
||||
|
||||
test("Second attendee reschedule from /upcoming page should use correct seatReferenceUid and show attendee info", async ({
|
||||
page,
|
||||
users,
|
||||
bookings,
|
||||
}) => {
|
||||
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
||||
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
]);
|
||||
|
||||
const bookingAttendees = await prisma.attendee.findMany({
|
||||
where: { bookingId: booking.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const bookingSeats = bookingAttendees.map((attendee) => ({
|
||||
bookingId: booking.id,
|
||||
attendeeId: attendee.id,
|
||||
referenceUid: uuidv4(),
|
||||
data: {
|
||||
responses: {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await prisma.bookingSeat.createMany({
|
||||
data: bookingSeats,
|
||||
});
|
||||
|
||||
const references = await prisma.bookingSeat.findMany({
|
||||
where: { bookingId: booking.id },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
|
||||
const secondUser = await users.create({
|
||||
name: "Jane Second",
|
||||
email: "second+seats@cal.com",
|
||||
});
|
||||
await secondUser.apiLogin();
|
||||
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.waitForSelector('[data-testid="bookings"]');
|
||||
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
await page.locator('[data-testid="reschedule"]').click();
|
||||
|
||||
await page.waitForURL((url) => {
|
||||
const rescheduleUid = url.searchParams.get("rescheduleUid");
|
||||
return !!rescheduleUid && rescheduleUid === references[1].referenceUid;
|
||||
});
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
const nameElement = page.locator("input[name=name]");
|
||||
const name = await nameElement.inputValue();
|
||||
expect(name).toBe("Jane Second");
|
||||
|
||||
const emailElement = page.locator("input[name=email]");
|
||||
const email = await emailElement.inputValue();
|
||||
expect(email).toBe("second+seats@cal.com");
|
||||
|
||||
// Complete the reschedule
|
||||
await confirmReschedule(page);
|
||||
|
||||
// Verify successful reschedule
|
||||
await page.waitForURL(/\/booking\/.*/);
|
||||
await expect(page).toHaveURL(/\/booking\/.*/);
|
||||
});
|
||||
|
||||
// @TODO: force 404 when rescheduleUid is not found
|
||||
});
|
||||
|
||||
@@ -60,7 +60,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
@@ -98,7 +97,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "floating-popup",
|
||||
@@ -136,7 +134,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "element-click",
|
||||
@@ -176,7 +173,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
@@ -234,7 +230,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
@@ -275,7 +270,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "floating-popup",
|
||||
@@ -315,7 +309,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "element-click",
|
||||
@@ -333,8 +326,7 @@ function chooseEmbedType(page: Page, embedType: EmbedType) {
|
||||
}
|
||||
|
||||
async function goToReactCodeTab(page: Page) {
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
// To prevent early timeo
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator("[data-testid=horizontal-tab-react]").click();
|
||||
}
|
||||
@@ -343,7 +335,7 @@ async function clickEmbedButton(page: Page) {
|
||||
const embedButton = page.locator("[data-testid=embed]");
|
||||
const embedUrl = await embedButton.getAttribute("data-test-embed-url");
|
||||
embedButton.click();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
return embedUrl!;
|
||||
}
|
||||
|
||||
@@ -439,18 +431,24 @@ async function expectValidHtmlEmbedSnippet(
|
||||
}
|
||||
|
||||
function assertThatCodeIsValidVanillaJsCode(code: string) {
|
||||
const lintResult = linter.verify(code, {
|
||||
env: {
|
||||
browser: true,
|
||||
const lintResult = linter.verify(code, [
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||
globals: {
|
||||
window: "readonly",
|
||||
document: "readonly",
|
||||
navigator: "readonly",
|
||||
Cal: "readonly",
|
||||
console: "readonly",
|
||||
},
|
||||
},
|
||||
rules: eslintRules,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
},
|
||||
globals: {
|
||||
Cal: "readonly",
|
||||
},
|
||||
rules: eslintRules,
|
||||
});
|
||||
]);
|
||||
|
||||
if (lintResult.length) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
@@ -459,23 +457,35 @@ function assertThatCodeIsValidVanillaJsCode(code: string) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
expect(lintResult.length).toBe(0);
|
||||
}
|
||||
|
||||
function assertThatCodeIsValidReactCode(code: string) {
|
||||
const lintResult = linter.verify(code, {
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
const lintResult = linter.verify(code, [
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
globals: {
|
||||
window: "readonly",
|
||||
document: "readonly",
|
||||
navigator: "readonly",
|
||||
console: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...eslintRules,
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
semi: "off",
|
||||
},
|
||||
sourceType: "module",
|
||||
},
|
||||
rules: eslintRules,
|
||||
});
|
||||
]);
|
||||
|
||||
if (lintResult.length) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
@@ -484,6 +494,7 @@ function assertThatCodeIsValidReactCode(code: string) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
expect(lintResult.length).toBe(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -2202,6 +2202,7 @@
|
||||
"booking_confirmation_failed": "Booking confirmation failed",
|
||||
"not_enough_seats": "Not enough seats",
|
||||
"form_builder_field_already_exists": "A field with this name already exists",
|
||||
"guests_field_must_be_multiemail": "Guests field must be of type 'Multiple emails'",
|
||||
"show_on_booking_page": "Show on booking page",
|
||||
"visit_cancelled_booking": "You can visit the canceled booking page",
|
||||
"get_started_zapier_templates": "Get started with Zapier templates",
|
||||
|
||||
@@ -647,3 +647,8 @@ select:focus {
|
||||
[data-radix-popper-content-wrapper] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .react-tel-input .flag-dropdown {
|
||||
left: auto !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
@@ -9,6 +7,13 @@ import {
|
||||
} from "@calcom/lib/intervalLimits/server/checkDurationLimits";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
|
||||
|
||||
const mockGetTotalBookingDuration = vi.fn();
|
||||
vi.mock("@calcom/lib/server/repository/booking", () => ({
|
||||
BookingRepository: vi.fn().mockImplementation(() => ({
|
||||
getTotalBookingDuration: mockGetTotalBookingDuration,
|
||||
})),
|
||||
}));
|
||||
|
||||
type MockData = {
|
||||
id: number;
|
||||
startDate: Date;
|
||||
@@ -22,19 +27,19 @@ const MOCK_DATA: MockData = {
|
||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||
describe("Check Duration Limits Tests", () => {
|
||||
it("Should return no errors if limit is not reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 0 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(0);
|
||||
await expect(
|
||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should throw an error if limit is reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||
await expect(
|
||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
it("Should pass with multiple duration limits", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(30);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -47,7 +52,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should pass with multiple duration limits with one undefined", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(30);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -60,7 +65,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should return no errors if limit is not reached with multiple bookings", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -73,7 +78,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should throw an error if one of the limit is reached with multiple bookings", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 90 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(90);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -90,7 +95,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||
describe("Check Duration Limit Tests", () => {
|
||||
it("Should return no busyTimes and no error if limit is not reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||
await expect(
|
||||
checkDurationLimit({
|
||||
key: "PER_DAY",
|
||||
|
||||
@@ -143,9 +143,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
...expectedEvType,
|
||||
parentId: 1,
|
||||
users: { connect: [{ id: 4 }] },
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
@@ -206,7 +207,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
bookingLimits: undefined,
|
||||
},
|
||||
});
|
||||
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
|
||||
const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
@@ -317,9 +318,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
...expectedEvType,
|
||||
parentId: 1,
|
||||
users: { connect: [{ id: 4 }] },
|
||||
bookingLimits: undefined,
|
||||
@@ -383,7 +385,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
length: 30,
|
||||
},
|
||||
});
|
||||
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
|
||||
const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
@@ -464,7 +466,11 @@ describe("handleChildrenEventTypes", () => {
|
||||
...evType,
|
||||
};
|
||||
|
||||
prismaMock.eventType.update.mockResolvedValue(mockUpdatedEventType);
|
||||
prismaMock.eventType.update.mockResolvedValue({
|
||||
...mockUpdatedEventType,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
@@ -480,9 +486,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
updatedValues: {},
|
||||
});
|
||||
|
||||
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
...expectedEvType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
@@ -507,7 +514,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
allowReschedulingCancelledBookings: false,
|
||||
},
|
||||
});
|
||||
const { profileId, rrSegmentQueryValue, ...rest } = evType;
|
||||
const { profileId, rrSegmentQueryValue, createdAt: _, updatedAt: __, ...rest } = evType;
|
||||
if ("workflows" in rest) delete rest.workflows;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
|
||||
@@ -1534,6 +1534,7 @@ export function getOrganizer({
|
||||
completedOnboarding,
|
||||
username,
|
||||
locked,
|
||||
emailVerified,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
@@ -1551,6 +1552,7 @@ export function getOrganizer({
|
||||
completedOnboarding?: boolean;
|
||||
username?: string;
|
||||
locked?: boolean;
|
||||
emailVerified?: Date | null;
|
||||
}) {
|
||||
username = username ?? TestData.users.example.username;
|
||||
return {
|
||||
@@ -1572,7 +1574,8 @@ export function getOrganizer({
|
||||
smsLockState,
|
||||
completedOnboarding,
|
||||
locked,
|
||||
};
|
||||
emailVerified,
|
||||
};
|
||||
}
|
||||
|
||||
export function getScenarioData(
|
||||
|
||||
+298
-465
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
import { config } from "@calcom/eslint-config/base";
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default nextJsConfig;
|
||||
@@ -6,10 +6,11 @@
|
||||
"dev": "PORT=5100 next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/atoms": "*",
|
||||
"@calcom/atoms": "workspace:*",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@prisma/client": "6.7.0",
|
||||
"next": "14.0.4",
|
||||
"prisma": "^6.7.0",
|
||||
@@ -22,8 +23,6 @@
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-next": "14.0.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^4.9.4"
|
||||
|
||||
+10
-9
@@ -8,6 +8,7 @@
|
||||
"packages/*",
|
||||
"packages/embeds/*",
|
||||
"packages/features/*",
|
||||
"packages/app-store",
|
||||
"packages/app-store/*",
|
||||
"packages/platform/*",
|
||||
"packages/platform/examples/base",
|
||||
@@ -53,9 +54,9 @@
|
||||
"env-check:common": "dotenv-checker --schema .env.example --env .env",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"heroku-postbuild": "turbo run @calcom/web#build",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"lint:report": "turbo run lint:report",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo lint:fix",
|
||||
"lint:report": "turbo lint:report",
|
||||
"lint": "turbo lint",
|
||||
"postinstall": "husky install && turbo run post-install",
|
||||
"pre-commit": "lint-staged",
|
||||
"predev": "echo 'Checking env files'",
|
||||
@@ -71,8 +72,8 @@
|
||||
"test-e2e:embed": "yarn db-seed && yarn e2e:embed",
|
||||
"test-e2e:embed-react": "yarn db-seed && yarn e2e:embed-react",
|
||||
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
||||
"test": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test": "TZ=UTC vitest run",
|
||||
"test:ui": "TZ=UTC vitest --ui",
|
||||
"type-check": "turbo run type-check",
|
||||
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||
"web": "yarn workspace @calcom/web",
|
||||
@@ -84,6 +85,7 @@
|
||||
"changesets-release": "NODE_OPTIONS='--max_old_space_size=12288' turbo run build --filter=@calcom/atoms && yarn changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "2.29.4",
|
||||
"@faker-js/faker": "9.2.0",
|
||||
@@ -96,7 +98,7 @@
|
||||
"c8": "^7.13.0",
|
||||
"checkly": "latest",
|
||||
"dotenv-checker": "^1.1.5",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint": "9.36.0",
|
||||
"husky": "^8.0.0",
|
||||
"i18n-unused": "^0.13.0",
|
||||
"jest-diff": "^29.5.0",
|
||||
@@ -110,6 +112,7 @@
|
||||
"prismock": "1.35.3",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"turbo": "^2.5.5",
|
||||
"typescript": "5.9.0-beta",
|
||||
"vitest": "^2.1.9",
|
||||
"vitest-fetch-mock": "^0.3.0",
|
||||
@@ -122,9 +125,7 @@
|
||||
"@vercel/functions": "^1.4.0",
|
||||
"city-timezones": "^1.2.1",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"eslint": "^8.34.0",
|
||||
"p-limit": "^6.2.0",
|
||||
"turbo": "^2.5.5"
|
||||
"p-limit": "^6.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"types-ramda": "0.29.4",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"dist/cli.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"ink": "^3.2.0",
|
||||
"ink-select-input": "^4.2.1",
|
||||
"ink-text-input": "^4.0.3",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@getalby/lightning-tools": "^5.1.2",
|
||||
"@getalby/sdk": "^4.0.0",
|
||||
"@webbtc/webln-types": "^3.0.0",
|
||||
@@ -13,7 +13,7 @@
|
||||
"svix": "^0.85.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Your Bitcoin & Nostr companion for the web. Use Alby to charge Satoshi for your Cal.com meetings.\r"
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"main": "./index.ts",
|
||||
"description": "The joyful productivity app\r\r",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Apple calendar runs both the macOS and iOS mobile operating systems. Offering online cloud backup of calendars using Apple’s iCloud service, it can sync with Google Calendar and Microsoft Exchange Server. Users can schedule events in their day that include time, location, duration, and extra notes.",
|
||||
"dependencies": {
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"react-hook-form": "^7.43.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Attio is the AI-native CRM that builds, scales and grows your company to the next level."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "You simply paste a Cal.com link of someone, select a frequency and we will take care of the scheduling for you.\r\r"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Request a signed Business Associate Agreement for your HIPAA compliance records"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Basecamp puts everything you need to get work done in one place. It's the calm, organized way to manage projects, work with clients, and communicate company-wide."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.1",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "24x7 AI receptionists to answer all inbound calls"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "BTCPay Server is a self-hosted open source Bitcoin payment processor. Start receiving bitcoin payments for your events and bookings."
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Caldav is a protocol that allows different clients/servers to access scheduling information on remote servers as well as schedule meetings with other users on the same server or other servers. It extends WebDAV specification and uses iCalendar format for the data.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"react-hook-form": "^7.43.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Feel connected with your remote team. Team events, new hire onboardings, coffee chats, all on Campfire. No more awkward Zoom calls.\r\r",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Add Cal.com to your Chatbase AI Chatbot and accept bookings right inside chat."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.1.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Create, List and Interact with Your Cal.com links and connections easily."
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*"
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "All of your commitments, now in one place."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Cal Video is the in-house web-based video conferencing platform powered by Daily.co, which is minimalistic and lightweight, but has most of the features you need.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*"
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Integrate Deel with your Calendar and get automatic updates on key employee dates like PTO, start days, public holidays, and birthdays. No manual entry. No errors. Just a calendar that's always in sync with your team."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.1",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Run Professional Video Meetings, Coach Sales Teams in Real-Time with AI, And Schedule Meetings on Auto-Pilot. 100% GDPR Compliant, Enterprise Ready."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "A new way to meet, with built-in Ai"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Discord is a VoIP and instant messaging social platform. Users have the ability to communicate with voice calls, video calls, text messaging, media and files in private chats or as part of communities"
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"dub-package": "npm:dub@^0.61.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Dub is the modern link attribution platform for you to create short links, track conversion analytics, and run affiliate programs."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "The best video conferencing solution for businesses of any size. Fully secure, reliable, packed with features and ridiculously simple to use.\r\r"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Element is an open-source communication platform that provides messaging, voice calling, and video conferencing capabilities. It is based on the Matrix protocol, which is a decentralized and federated messaging protocol designed to enable secure and interoperable communication across different platforms and services.\""
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Create the most realistic speech with our AI audio platform"
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"main": "./index.ts",
|
||||
"description": "For calendars hosted on on-premises Microsoft Exchange 2013 servers",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"ews-javascript-api": "^0.11.0",
|
||||
"react-hook-form": "^7.43.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"main": "./index.ts",
|
||||
"description": "For calendars hosted on on-premises Microsoft Exchange 2016 servers",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"ews-javascript-api": "^0.11.0",
|
||||
"react-hook-form": "^7.43.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS).",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@ewsjs/xhr": "^3.1.2",
|
||||
"ews-javascript-api": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Facetime makes it super simple for collaborating teams to jump on a video call.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Fathom Analytics provides simple, privacy-focused website analytics. We're a GDPR-compliant, Google Analytics alternative.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Feishu Calendar is a time management and scheduling service developed by Feishu. Allows users to create and edit events, with options available for type and time. Available to anyone that has a Feishu account on both mobile and web versions.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*"
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Google Analytics is a web analytics service offered by Google that tracks and reports website traffic, currently as a platform inside the Google Marketing Platform brand.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user