Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d431607125 | ||
|
|
72253021ef | ||
|
|
be0cc31d63 | ||
|
|
38e67ccc79 | ||
|
|
3fe26f19ff | ||
|
|
f38f98a0b6 | ||
|
|
b4d5f511b5 | ||
|
|
86fc5161b6 | ||
|
|
dbf4be27ea | ||
|
|
7b69a6ab9c | ||
|
|
f897c680a4 | ||
|
|
ac05693694 | ||
|
|
3174b55e21 | ||
|
|
08f4a48774 | ||
|
|
928dd49965 | ||
|
|
47a8ba1eaa | ||
|
|
c4e78c213b | ||
|
|
a31cf9963d | ||
|
|
7ff255d1e7 | ||
|
|
4a88051531 | ||
|
|
765b0b44f8 | ||
|
|
10f8b40348 | ||
|
|
c9a9f70801 | ||
|
|
c2d7054ec4 | ||
|
|
89ac9a3d31 | ||
|
|
7d9c7a8c22 | ||
|
|
48e7efccb2 | ||
|
|
0072aba55d | ||
|
|
c7a96e1b43 | ||
|
|
0288ea931f | ||
|
|
c958b6017a | ||
|
|
ffe03be018 | ||
|
|
f760a8ac26 | ||
|
|
d0a7eb12a0 | ||
|
|
9a473d5b55 | ||
|
|
c5a83f4df2 | ||
|
|
3e175dbca3 | ||
|
|
762b995172 | ||
|
|
d2d7453962 |
No files matched your search
@@ -19,11 +19,6 @@
|
|||||||
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
|
||||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
|
||||||
CAL_SIGNATURE_TOKEN=
|
|
||||||
# The route to the Cal.com License API
|
|
||||||
CALCOM_PRIVATE_API_ROUTE=
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ runs:
|
|||||||
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
||||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||||
key-4: ${{ github.sha }}
|
key-4: ${{ github.sha }}
|
||||||
|
key-5: ${{ github.event.pull_request.head.sha }}
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ github.workspace }}/apps/web/.next
|
${{ github.workspace }}/apps/web/.next
|
||||||
${{ github.workspace }}/apps/web/public/embed
|
${{ github.workspace }}/apps/web/public/embed
|
||||||
**/.turbo/**
|
**/.turbo/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}-${{ env.key-5 }}
|
||||||
- run: |
|
- run: |
|
||||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
yarn build
|
yarn build
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- name: Cache API v1 production build
|
- name: Cache API v1 production build
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
id: cache-api-v1-build
|
id: cache-api-v1-build
|
||||||
env:
|
env:
|
||||||
cache-name: api-v1-build
|
cache-name: api-v1-build
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- name: Cache API v2 production build
|
- name: Cache API v2 production build
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
id: cache-api-v2-build
|
id: cache-api-v2-build
|
||||||
env:
|
env:
|
||||||
cache-name: api-v2-build
|
cache-name: api-v2-build
|
||||||
|
|||||||
@@ -45,13 +45,7 @@ env:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
|
||||||
name: Production builds
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
|
||||||
secrets: inherit
|
|
||||||
analyze:
|
analyze:
|
||||||
needs: build
|
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: PR Labeled with ready-for-e2e
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [labeled]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- gh-actions-test-branch
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
run-e2e-jobs:
|
||||||
|
name: Run E2E Jobs
|
||||||
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
|
- name: Check Label and Retrigger Checks
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const labelName = context.payload.label.name;
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
|
||||||
|
if (labelName === 'ready-for-e2e') {
|
||||||
|
console.log(`Running E2E jobs for PR #${prNumber}...`);
|
||||||
|
|
||||||
|
const checkRuns = await github.rest.checks.listForRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: context.payload.pull_request.head.sha,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const check of checkRuns.data.check_runs) {
|
||||||
|
console.log('check.name', check.name);
|
||||||
|
if (check.name.includes('e2e')) {
|
||||||
|
await github.rest.actions.reRunJob({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
job_id: check.id,
|
||||||
|
});
|
||||||
|
console.log(`Triggering job #${check.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Triggered E2E checks for PR #${prNumber}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Label ${labelName} does not trigger re-running checks.`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: PR Reviewed
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label-pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Label PR as ready for E2E
|
||||||
|
if: github.event.review.state == 'approved'
|
||||||
|
uses: actions-ecosystem/action-add-labels@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
labels: 'ready-for-e2e'
|
||||||
+117
-6
@@ -4,6 +4,7 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- gh-actions-test-branch
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -18,6 +19,7 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
outputs:
|
outputs:
|
||||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||||
|
commit-sha: ${{ steps.get_sha.outputs.commit-sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
@@ -27,37 +29,146 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
has-files-requiring-all-checks:
|
has-files-requiring-all-checks:
|
||||||
- "!(**.md|.github/CODEOWNERS)"
|
- "!(**.md|.github/CODEOWNERS)"
|
||||||
|
- name: Get Latest Commit SHA
|
||||||
|
id: get_sha
|
||||||
|
run: |
|
||||||
|
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
check-label:
|
||||||
|
needs: [changes]
|
||||||
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
name: Check for E2E label
|
||||||
|
outputs:
|
||||||
|
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || (github.event.action == 'labeled' && github.event.label.name == 'ready-for-e2e')) }}
|
||||||
|
steps:
|
||||||
|
- name: Check if PR exists with ready-for-e2e label for this SHA
|
||||||
|
id: check-if-pr-has-label
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let labels = [];
|
||||||
|
|
||||||
|
if (context.payload.pull_request) {
|
||||||
|
labels = context.payload.pull_request.labels;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const sha = '${{ needs.changes.outputs.commit-sha }}';
|
||||||
|
console.log('sha', sha);
|
||||||
|
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
commit_sha: sha
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prs.length === 0) {
|
||||||
|
core.setOutput('run-e2e', false);
|
||||||
|
console.log(`No pull requests found for commit SHA ${sha}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = prs[0];
|
||||||
|
console.log(`PR number: ${pr.number}`);
|
||||||
|
console.log(`PR title: ${pr.title}`);
|
||||||
|
console.log(`PR state: ${pr.state}`);
|
||||||
|
console.log(`PR URL: ${pr.html_url}`);
|
||||||
|
|
||||||
|
labels = pr.labels;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
core.setOutput('run-e2e', false);
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelFound = labels.map(l => l.name).includes('ready-for-e2e');
|
||||||
|
console.log('Found the label?', labelFound);
|
||||||
|
core.setOutput('run-e2e', labelFound);
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
name: Type check
|
name: Type check
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/check-types.yml
|
uses: ./.github/workflows/check-types.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Linters
|
name: Linters
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
unit-test:
|
unit-test:
|
||||||
name: Tests
|
name: Tests
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/unit-tests.yml
|
uses: ./.github/workflows/unit-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
integration-test:
|
integration-test:
|
||||||
name: Tests
|
name: Tests
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/integration-tests.yml
|
uses: ./.github/workflows/integration-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-api-v1:
|
||||||
|
name: Production builds
|
||||||
|
needs: [changes, check-label]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/api-v1-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build-api-v2:
|
||||||
|
name: Production builds
|
||||||
|
needs: [changes, check-label]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/api-v2-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Production builds
|
||||||
|
needs: [changes, check-label]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: e2e
|
||||||
|
needs: [changes, check-label, build]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/e2e.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-app-store:
|
||||||
|
name: e2e-app-store
|
||||||
|
needs: [changes, check-label, build]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/e2e-app-store.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed:
|
||||||
|
name: e2e-embed
|
||||||
|
needs: [changes, check-label, build]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed-react:
|
||||||
|
name: e2e-embed-react
|
||||||
|
needs: [changes, check-label, build]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
analyze:
|
||||||
|
name: Analyze Build
|
||||||
|
needs: [build]
|
||||||
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [changes, lint, type-check, unit-test, integration-test]
|
needs: [changes, lint, type-check, unit-test, integration-test, check-label, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
if: always()
|
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export function register() {
|
|||||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
// reduce sample rate to 10% on production
|
|
||||||
tracesSampleRate: process.env.NODE_ENV !== "production" ? 1.0 : 0.1,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiResponse, NextApiRequest } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
|
||||||
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
|
|
||||||
|
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
|
||||||
|
checkRateLimitAndThrowError: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("rateLimitApiKey middleware", () => {
|
||||||
|
it("should return 401 if no apiKey is provided", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(res._getStatusCode()).toBe(401);
|
||||||
|
expect(res._getJSONData()).toEqual({ message: "No apiKey provided" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call checkRateLimitAndThrowError with correct parameters", async () => {
|
||||||
|
const { req, res } = createMocks({
|
||||||
|
method: "GET",
|
||||||
|
query: { apiKey: "test-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
|
||||||
|
limit: 100,
|
||||||
|
remaining: 99,
|
||||||
|
reset: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error weird typing between middleware and createMocks
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
||||||
|
identifier: "test-key",
|
||||||
|
rateLimitingType: "api",
|
||||||
|
onRateLimiterResponse: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set rate limit headers correctly", async () => {
|
||||||
|
const { req, res } = createMocks({
|
||||||
|
method: "GET",
|
||||||
|
query: { apiKey: "test-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const rateLimiterResponse = {
|
||||||
|
limit: 100,
|
||||||
|
remaining: 99,
|
||||||
|
reset: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||||
|
onRateLimiterResponse(rateLimiterResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error weird typing between middleware and createMocks
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
||||||
|
expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining);
|
||||||
|
expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 429 if rate limit is exceeded", async () => {
|
||||||
|
const { req, res } = createMocks({
|
||||||
|
method: "GET",
|
||||||
|
query: { apiKey: "test-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
|
||||||
|
|
||||||
|
// @ts-expect-error weird typing between middleware and createMocks
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(res._getStatusCode()).toBe(429);
|
||||||
|
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,15 +6,19 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
|||||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||||
|
|
||||||
// TODO: Add a way to add trusted api keys
|
// TODO: Add a way to add trusted api keys
|
||||||
await checkRateLimitAndThrowError({
|
try {
|
||||||
identifier: req.query.apiKey as string,
|
await checkRateLimitAndThrowError({
|
||||||
rateLimitingType: "api",
|
identifier: req.query.apiKey as string,
|
||||||
onRateLimiterResponse: (response) => {
|
rateLimitingType: "api",
|
||||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
onRateLimiterResponse: (response) => {
|
||||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||||
},
|
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(429).json({ message: "Rate limit exceeded" });
|
||||||
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
@@ -20,3 +20,7 @@ STRIPE_API_KEY=
|
|||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|
||||||
WEB_APP_URL=http://localhost:3000/
|
WEB_APP_URL=http://localhost:3000/
|
||||||
|
CALCOM_LICENSE_KEY=
|
||||||
|
API_KEY_PREFIX=cal_
|
||||||
|
GET_LICENSE_KEY_URL="https://console.cal.com/api/license"
|
||||||
|
IS_E2E=false
|
||||||
@@ -42,6 +42,16 @@ $ yarn prisma generate
|
|||||||
|
|
||||||
Copy `.env.example` to `.env` and fill values.
|
Copy `.env.example` to `.env` and fill values.
|
||||||
|
|
||||||
|
## Add license Key to deployments table in DB
|
||||||
|
|
||||||
|
id, logo theme licenseKey agreedLicenseAt
|
||||||
|
1, null, null, 'c4234812-12ab-42s6-a1e3-55bedd4a5bb7', '2023-05-15 21:39:47.611'
|
||||||
|
|
||||||
|
your CALCOM_LICENSE_KEY env var need to contain the same value
|
||||||
|
|
||||||
|
.env
|
||||||
|
CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
|
||||||
|
|
||||||
## Running the app
|
## Running the app
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
|
"@calcom/platform-libraries-0.0.13": "npm:@calcom/platform-libraries@0.0.13",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||||
"@calcom/platform-libraries-0.0.4": "npm:@calcom/platform-libraries@0.0.4",
|
"@calcom/platform-libraries-0.0.4": "npm:@calcom/platform-libraries@0.0.4",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
type: VersioningType.CUSTOM,
|
type: VersioningType.CUSTOM,
|
||||||
extractor: (request: unknown) => {
|
extractor: (request: unknown) => {
|
||||||
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
|
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
|
||||||
console.log("asap header headerVersion", headerVersion);
|
|
||||||
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
|
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
|
||||||
console.log("asap return header headerVersion", headerVersion);
|
|
||||||
return headerVersion;
|
return headerVersion;
|
||||||
}
|
}
|
||||||
return VERSION_2024_04_15;
|
return VERSION_2024_04_15;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ const loadConfig = (): AppConfig => {
|
|||||||
? `:${Number(getEnv("API_PORT", "5555"))}`
|
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||||
: ""
|
: ""
|
||||||
}/v2`,
|
}/v2`,
|
||||||
|
keyPrefix: getEnv("API_KEY_PREFIX", "cal_"),
|
||||||
|
licenseKey: getEnv("CALCOM_LICENSE_KEY", ""),
|
||||||
|
licenseKeyUrl: getEnv("GET_LICENSE_KEY_URL", "https://console.cal.com/api/license"),
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
readUrl: getEnv("DATABASE_READ_URL"),
|
readUrl: getEnv("DATABASE_READ_URL"),
|
||||||
@@ -31,6 +34,7 @@ const loadConfig = (): AppConfig => {
|
|||||||
app: {
|
app: {
|
||||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||||
},
|
},
|
||||||
|
e2e: getEnv("IS_E2E", false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export type AppConfig = {
|
|||||||
port: number;
|
port: number;
|
||||||
path: string;
|
path: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
keyPrefix: string;
|
||||||
|
licenseKey: string;
|
||||||
|
licenseKeyUrl: string;
|
||||||
};
|
};
|
||||||
db: {
|
db: {
|
||||||
readUrl: string;
|
readUrl: string;
|
||||||
@@ -22,4 +25,5 @@ export type AppConfig = {
|
|||||||
app: {
|
app: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
};
|
};
|
||||||
|
e2e: boolean;
|
||||||
};
|
};
|
||||||
@@ -16,7 +16,7 @@ import * as request from "supertest";
|
|||||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { handleNewBooking } from "@calcom/platform-libraries-0.0.2";
|
import { handleNewBooking } from "@calcom/platform-libraries-0.0.2";
|
||||||
@@ -39,7 +39,7 @@ describe("Bookings Endpoints", () => {
|
|||||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withApiAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
|||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
@@ -89,7 +89,7 @@ export class BookingsController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@Permissions([BOOKING_READ])
|
@Permissions([BOOKING_READ])
|
||||||
@ApiQuery({ name: "filters[status]", enum: Status, required: true })
|
@ApiQuery({ name: "filters[status]", enum: Status, required: true })
|
||||||
@ApiQuery({ name: "limit", type: "number", required: false })
|
@ApiQuery({ name: "limit", type: "number", required: false })
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
|||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +42,7 @@ export class CalendarsController {
|
|||||||
private readonly googleCalendarService: GoogleCalendarService
|
private readonly googleCalendarService: GoogleCalendarService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@Get("/busy-times")
|
@Get("/busy-times")
|
||||||
async getBusyTimes(
|
async getBusyTimes(
|
||||||
@Query() queryParams: CalendarBusyTimesInput,
|
@Query() queryParams: CalendarBusyTimesInput,
|
||||||
@@ -71,7 +71,7 @@ export class CalendarsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
|
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
|
||||||
const calendars = await this.calendarsService.getCalendars(userId);
|
const calendars = await this.calendarsService.getCalendars(userId);
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export class CalendarsController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@Get("/:calendar/connect")
|
@Get("/:calendar/connect")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async redirect(
|
async redirect(
|
||||||
@@ -135,7 +135,7 @@ export class CalendarsController {
|
|||||||
|
|
||||||
@Get("/:calendar/check")
|
@Get("/:calendar/check")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@Permissions([APPS_READ])
|
@Permissions([APPS_READ])
|
||||||
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
|
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
|
||||||
switch (calendar) {
|
switch (calendar) {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { EventTypesController } from "@/ee/event-types/controllers/event-types.controller";
|
|
||||||
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
|
||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
|
||||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
|
||||||
import { Module } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
|
||||||
providers: [EventTypesRepository, EventTypesService],
|
|
||||||
controllers: [EventTypesController],
|
|
||||||
exports: [EventTypesService, EventTypesRepository],
|
|
||||||
})
|
|
||||||
export class EventTypesModule {}
|
|
||||||
File renamed without changes.
+59
-25
@@ -1,13 +1,13 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
import { Editable } from "@/ee/event-types/event-types_2024_04_15//inputs/enums/editable";
|
||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||||
import { Editable } from "@/ee/event-types/inputs/enums/editable";
|
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
|
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
|
||||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output";
|
||||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output";
|
||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
@@ -22,9 +22,14 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
|
|||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import {
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
VERSION_2024_06_11,
|
||||||
|
VERSION_2024_04_15,
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
import {
|
import {
|
||||||
EventTypesByViewer,
|
EventTypesByViewer,
|
||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
@@ -40,7 +45,7 @@ describe("Event types Endpoints", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
imports: [AppModule, UsersModule, EventTypesModule, TokensModule],
|
imports: [AppModule, UsersModule, EventTypesModule_2024_04_15, TokensModule],
|
||||||
})
|
})
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
.useValue({
|
.useValue({
|
||||||
@@ -79,11 +84,11 @@ describe("Event types Endpoints", () => {
|
|||||||
let user: User;
|
let user: User;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withApiAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
imports: [AppModule, UsersModule, EventTypesModule, TokensModule],
|
imports: [AppModule, UsersModule, EventTypesModule_2024_04_15, TokensModule],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -132,7 +137,7 @@ describe("Event types Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should create an event type", async () => {
|
it("should create an event type", async () => {
|
||||||
const body: CreateEventTypeInput = {
|
const body: CreateEventTypeInput_2024_04_15 = {
|
||||||
title: "Test Event Type",
|
title: "Test Event Type",
|
||||||
slug: "test-event-type",
|
slug: "test-event-type",
|
||||||
description: "A description of the test event type.",
|
description: "A description of the test event type.",
|
||||||
@@ -154,6 +159,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/api/v2/event-types")
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
@@ -173,7 +179,7 @@ describe("Event types Endpoints", () => {
|
|||||||
it("should update event type", async () => {
|
it("should update event type", async () => {
|
||||||
const newTitle = "Updated title";
|
const newTitle = "Updated title";
|
||||||
|
|
||||||
const body: UpdateEventTypeInput = {
|
const body: UpdateEventTypeInput_2024_04_15 = {
|
||||||
title: newTitle,
|
title: newTitle,
|
||||||
disableGuests: false,
|
disableGuests: false,
|
||||||
slotInterval: 30,
|
slotInterval: 30,
|
||||||
@@ -184,6 +190,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
@@ -207,7 +214,7 @@ describe("Event types Endpoints", () => {
|
|||||||
it("should return 400 if param event type id is null", async () => {
|
it("should return 400 if param event type id is null", async () => {
|
||||||
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
||||||
|
|
||||||
const body: UpdateEventTypeInput = {
|
const body: UpdateEventTypeInput_2024_04_15 = {
|
||||||
locations,
|
locations,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -217,7 +224,7 @@ describe("Event types Endpoints", () => {
|
|||||||
it("should update event type locations", async () => {
|
it("should update event type locations", async () => {
|
||||||
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
||||||
|
|
||||||
const body: UpdateEventTypeInput = {
|
const body: UpdateEventTypeInput_2024_04_15 = {
|
||||||
locations,
|
locations,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,7 +273,7 @@ describe("Event types Endpoints", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const body: UpdateEventTypeInput = {
|
const body: UpdateEventTypeInput_2024_04_15 = {
|
||||||
bookingFields,
|
bookingFields,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,7 +295,26 @@ describe("Event types Endpoints", () => {
|
|||||||
it(`/GET/:id`, async () => {
|
it(`/GET/:id`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/${eventType.id}`)
|
.get(`/api/v2/event-types/${eventType.id}`)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
|
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||||
|
.set("Authorization", `Bearer whatever`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetEventTypeOutput = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.eventType.id).toEqual(eventType.id);
|
||||||
|
expect(responseBody.data.eventType.title).toEqual(eventType.title);
|
||||||
|
expect(responseBody.data.eventType.slug).toEqual(eventType.slug);
|
||||||
|
expect(responseBody.data.eventType.userId).toEqual(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/:id with version VERSION_2024_06_11`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||||
.set("Authorization", `Bearer whatever`)
|
.set("Authorization", `Bearer whatever`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@@ -305,7 +331,8 @@ describe("Event types Endpoints", () => {
|
|||||||
it(`/GET/:username/public`, async () => {
|
it(`/GET/:username/public`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/${username}/public`)
|
.get(`/api/v2/event-types/${username}/public`)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
|
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||||
.set("Authorization", `Bearer whatever`)
|
.set("Authorization", `Bearer whatever`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@@ -323,7 +350,8 @@ describe("Event types Endpoints", () => {
|
|||||||
it(`/GET/:username/:eventSlug/public`, async () => {
|
it(`/GET/:username/:eventSlug/public`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
|
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
|
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||||
.set("Authorization", `Bearer whatever`)
|
.set("Authorization", `Bearer whatever`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@@ -340,7 +368,8 @@ describe("Event types Endpoints", () => {
|
|||||||
it(`/GET/`, async () => {
|
it(`/GET/`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types`)
|
.get(`/api/v2/event-types`)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
|
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||||
.set("Authorization", `Bearer whatever`)
|
.set("Authorization", `Bearer whatever`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@@ -359,7 +388,8 @@ describe("Event types Endpoints", () => {
|
|||||||
it(`/GET/public/:username/`, async () => {
|
it(`/GET/public/:username/`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/${username}/public`)
|
.get(`/api/v2/event-types/${username}/public`)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
|
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||||
.set("Authorization", `Bearer whatever`)
|
.set("Authorization", `Bearer whatever`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@@ -375,13 +405,17 @@ describe("Event types Endpoints", () => {
|
|||||||
it(`/GET/:id not existing`, async () => {
|
it(`/GET/:id not existing`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/1000`)
|
.get(`/api/v2/event-types/1000`)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
|
// note: bearer token value mocked using "withApiAuth" for user which id is used when creating event type above
|
||||||
.set("Authorization", `Bearer whatever`)
|
.set("Authorization", `Bearer whatever`)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should delete schedule", async () => {
|
it("should delete schedule", async () => {
|
||||||
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
+30
-29
@@ -1,19 +1,22 @@
|
|||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { EventTypeIdParams } from "@/ee/event-types/inputs/event-type-id.input";
|
import { EventTypeIdParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input";
|
||||||
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
import { GetPublicEventTypeQueryParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
import { CreateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output";
|
||||||
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
import { DeleteEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output";
|
||||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
|
||||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output";
|
||||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output";
|
||||||
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
import {
|
||||||
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
GetEventTypesData,
|
||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
GetEventTypesOutput,
|
||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
} from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output";
|
||||||
|
import { UpdateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output";
|
||||||
|
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||||
|
import { VERSION_2024_04_15, VERSION_2024_06_11 } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
@@ -42,21 +45,21 @@ import { PrismaClient } from "@calcom/prisma";
|
|||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/event-types",
|
path: "/v2/event-types",
|
||||||
version: API_VERSIONS_VALUES,
|
version: [VERSION_2024_04_15, VERSION_2024_06_11],
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Event types")
|
@DocsTags("Event types")
|
||||||
export class EventTypesController {
|
export class EventTypesController_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesService: EventTypesService,
|
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||||
private readonly prismaReadService: PrismaReadService
|
private readonly prismaReadService: PrismaReadService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async createEventType(
|
async createEventType(
|
||||||
@Body() body: CreateEventTypeInput,
|
@Body() body: CreateEventTypeInput_2024_04_15,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<CreateEventTypeOutput> {
|
): Promise<CreateEventTypeOutput> {
|
||||||
const eventType = await this.eventTypesService.createUserEventType(user, body);
|
const eventType = await this.eventTypesService.createUserEventType(user, body);
|
||||||
@@ -69,11 +72,9 @@ export class EventTypesController {
|
|||||||
|
|
||||||
@Get("/:eventTypeId")
|
@Get("/:eventTypeId")
|
||||||
@Permissions([EVENT_TYPE_READ])
|
@Permissions([EVENT_TYPE_READ])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async getEventType(
|
async getEventType(
|
||||||
@Param() params: EventTypeIdParams,
|
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<GetEventTypeOutput> {
|
): Promise<GetEventTypeOutput> {
|
||||||
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
||||||
@@ -90,7 +91,7 @@ export class EventTypesController {
|
|||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([EVENT_TYPE_READ])
|
@Permissions([EVENT_TYPE_READ])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
|
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
|
||||||
const eventTypes = await getEventTypesByViewer({
|
const eventTypes = await getEventTypesByViewer({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -109,7 +110,7 @@ export class EventTypesController {
|
|||||||
async getPublicEventType(
|
async getPublicEventType(
|
||||||
@Param("username") username: string,
|
@Param("username") username: string,
|
||||||
@Param("eventSlug") eventSlug: string,
|
@Param("eventSlug") eventSlug: string,
|
||||||
@Query() queryParams: GetPublicEventTypeQueryParams
|
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15
|
||||||
): Promise<GetEventTypePublicOutput> {
|
): Promise<GetEventTypePublicOutput> {
|
||||||
try {
|
try {
|
||||||
const event = await getPublicEvent(
|
const event = await getPublicEvent(
|
||||||
@@ -146,12 +147,12 @@ export class EventTypesController {
|
|||||||
|
|
||||||
@Patch("/:eventTypeId")
|
@Patch("/:eventTypeId")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async updateEventType(
|
async updateEventType(
|
||||||
@Param() params: EventTypeIdParams,
|
@Param() params: EventTypeIdParams_2024_04_15,
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Body() body: UpdateEventTypeInput,
|
@Body() body: UpdateEventTypeInput_2024_04_15,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<UpdateEventTypeOutput> {
|
): Promise<UpdateEventTypeOutput> {
|
||||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||||
@@ -164,9 +165,9 @@ export class EventTypesController {
|
|||||||
|
|
||||||
@Delete("/:eventTypeId")
|
@Delete("/:eventTypeId")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async deleteEventType(
|
async deleteEventType(
|
||||||
@Param() params: EventTypeIdParams,
|
@Param() params: EventTypeIdParams_2024_04_15,
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@GetUser("id") userId: number
|
@GetUser("id") userId: number
|
||||||
): Promise<DeleteEventTypeOutput> {
|
): Promise<DeleteEventTypeOutput> {
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/controllers/event-types.controller";
|
||||||
|
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||||
|
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
||||||
|
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
||||||
|
controllers: [EventTypesController_2024_04_15],
|
||||||
|
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
||||||
|
})
|
||||||
|
export class EventTypesModule_2024_04_15 {}
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
@@ -8,12 +8,12 @@ import { getEventTypeById } from "@calcom/platform-libraries-0.0.2";
|
|||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesRepository {
|
export class EventTypesRepository_2024_04_15 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async createUserEventType(
|
async createUserEventType(
|
||||||
userId: number,
|
userId: number,
|
||||||
body: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">
|
body: Pick<CreateEventTypeInput_2024_04_15, "title" | "slug" | "length" | "hidden">
|
||||||
) {
|
) {
|
||||||
return this.dbWrite.prisma.eventType.create({
|
return this.dbWrite.prisma.eventType.create({
|
||||||
data: {
|
data: {
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +20,7 @@ export const CREATE_EVENT_DESCRIPTION_EXAMPLE =
|
|||||||
|
|
||||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||||
// Just uncomment any below when requested.
|
// Just uncomment any below when requested.
|
||||||
export class CreateEventTypeInput {
|
export class CreateEventTypeInput_2024_04_15 {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||||
@@ -46,9 +46,9 @@ export class CreateEventTypeInput {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => EventTypeLocation)
|
@Type(() => EventTypeLocation_2024_04_15)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
locations?: EventTypeLocation[];
|
locations?: EventTypeLocation_2024_04_15[];
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { IsNumberString } from "class-validator";
|
import { IsNumberString } from "class-validator";
|
||||||
|
|
||||||
export class EventTypeIdParams {
|
export class EventTypeIdParams_2024_04_15 {
|
||||||
@IsNumberString()
|
@IsNumberString()
|
||||||
eventTypeId!: number;
|
eventTypeId!: number;
|
||||||
}
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato
|
|||||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||||
// Just uncomment any below when requested.
|
// Just uncomment any below when requested.
|
||||||
|
|
||||||
export class EventTypeLocation {
|
export class EventTypeLocation_2024_04_15 {
|
||||||
@IsString()
|
@IsString()
|
||||||
@DocsProperty({ example: "link" })
|
@DocsProperty({ example: "link" })
|
||||||
type!: string;
|
type!: string;
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
|||||||
import { Transform } from "class-transformer";
|
import { Transform } from "class-transformer";
|
||||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
export class GetPublicEventTypeQueryParams {
|
export class GetPublicEventTypeQueryParams_2024_04_15 {
|
||||||
@Transform(({ value }: { value: string }) => value === "true")
|
@Transform(({ value }: { value: string }) => value === "true")
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
+12
-12
@@ -1,7 +1,7 @@
|
|||||||
import { Editable } from "@/ee/event-types/inputs/enums/editable";
|
import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable";
|
||||||
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
|
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||||
import { Frequency } from "@/ee/event-types/inputs/enums/frequency";
|
import { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency";
|
||||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import {
|
import {
|
||||||
IsString,
|
IsString,
|
||||||
@@ -105,7 +105,7 @@ class VariantsConfig {
|
|||||||
variants!: Record<string, Variant>;
|
variants!: Record<string, Variant>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BookingField {
|
export class BookingField_2024_04_15 {
|
||||||
@IsEnum(BaseField)
|
@IsEnum(BaseField)
|
||||||
type!: BaseField;
|
type!: BaseField;
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ export class BookingField {
|
|||||||
sources?: Source[];
|
sources?: Source[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecurringEvent {
|
export class RecurringEvent_2024_04_15 {
|
||||||
@IsDate()
|
@IsDate()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
dtstart?: Date;
|
dtstart?: Date;
|
||||||
@@ -202,7 +202,7 @@ export class RecurringEvent {
|
|||||||
tzid?: string;
|
tzid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IntervalLimits {
|
export class IntervalLimits_2024_04_15 {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
PER_DAY?: number;
|
PER_DAY?: number;
|
||||||
@@ -220,7 +220,7 @@ export class IntervalLimits {
|
|||||||
PER_YEAR?: number;
|
PER_YEAR?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateEventTypeInput {
|
export class UpdateEventTypeInput_2024_04_15 {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -243,9 +243,9 @@ export class UpdateEventTypeInput {
|
|||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => EventTypeLocation)
|
@Type(() => EventTypeLocation_2024_04_15)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
locations?: EventTypeLocation[];
|
locations?: EventTypeLocation_2024_04_15[];
|
||||||
|
|
||||||
// @IsInt()
|
// @IsInt()
|
||||||
// @IsOptional()
|
// @IsOptional()
|
||||||
@@ -278,8 +278,8 @@ export class UpdateEventTypeInput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => BookingField)
|
@Type(() => BookingField_2024_04_15)
|
||||||
bookingFields?: BookingField[];
|
bookingFields?: BookingField_2024_04_15[];
|
||||||
|
|
||||||
// @IsString()
|
// @IsString()
|
||||||
// @IsOptional()
|
// @IsOptional()
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import {
|
|||||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||||
CREATE_EVENT_SLUG_EXAMPLE,
|
CREATE_EVENT_SLUG_EXAMPLE,
|
||||||
CREATE_EVENT_TITLE_EXAMPLE,
|
CREATE_EVENT_TITLE_EXAMPLE,
|
||||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
+18
-18
@@ -3,15 +3,15 @@ import {
|
|||||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||||
CREATE_EVENT_SLUG_EXAMPLE,
|
CREATE_EVENT_SLUG_EXAMPLE,
|
||||||
CREATE_EVENT_TITLE_EXAMPLE,
|
CREATE_EVENT_TITLE_EXAMPLE,
|
||||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
|
import { PeriodType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/period-type";
|
||||||
import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
import { SchedulingType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type";
|
||||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||||
import {
|
import {
|
||||||
BookingField,
|
BookingField_2024_04_15,
|
||||||
IntervalLimits,
|
IntervalLimits_2024_04_15,
|
||||||
RecurringEvent,
|
RecurringEvent_2024_04_15,
|
||||||
} from "@/ee/event-types/inputs/update-event-type.input";
|
} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import {
|
import {
|
||||||
@@ -53,9 +53,9 @@ export class EventTypeOutput {
|
|||||||
hidden!: boolean;
|
hidden!: boolean;
|
||||||
|
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => EventTypeLocation)
|
@Type(() => EventTypeLocation_2024_04_15)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
locations!: EventTypeLocation[] | null;
|
locations!: EventTypeLocation_2024_04_15[] | null;
|
||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
@@ -91,9 +91,9 @@ export class EventTypeOutput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => BookingField)
|
@Type(() => BookingField_2024_04_15)
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
bookingFields!: BookingField[] | null;
|
bookingFields!: BookingField_2024_04_15[] | null;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
@@ -132,10 +132,10 @@ export class EventTypeOutput {
|
|||||||
requiresBookerEmailVerification!: boolean;
|
requiresBookerEmailVerification!: boolean;
|
||||||
|
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => RecurringEvent)
|
@Type(() => RecurringEvent_2024_04_15)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
recurringEvent!: RecurringEvent | null;
|
recurringEvent!: RecurringEvent_2024_04_15 | null;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
@@ -203,15 +203,15 @@ export class EventTypeOutput {
|
|||||||
successRedirectUrl!: string | null;
|
successRedirectUrl!: string | null;
|
||||||
|
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => IntervalLimits)
|
@Type(() => IntervalLimits_2024_04_15)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
bookingLimits!: IntervalLimits;
|
bookingLimits!: IntervalLimits_2024_04_15;
|
||||||
|
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => IntervalLimits)
|
@Type(() => IntervalLimits_2024_04_15)
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
durationLimits!: IntervalLimits;
|
durationLimits!: IntervalLimits_2024_04_15;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, ValidateNested } from "class-validator";
|
import { IsEnum, ValidateNested } from "class-validator";
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import {
|
|||||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||||
CREATE_EVENT_SLUG_EXAMPLE,
|
CREATE_EVENT_SLUG_EXAMPLE,
|
||||||
CREATE_EVENT_TITLE_EXAMPLE,
|
CREATE_EVENT_TITLE_EXAMPLE,
|
||||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsArray, IsEnum, ValidateNested } from "class-validator";
|
import { IsArray, IsEnum, ValidateNested } from "class-validator";
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
+13
-10
@@ -1,8 +1,8 @@
|
|||||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
|
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
|
||||||
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
@@ -14,16 +14,19 @@ import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-librarie
|
|||||||
import { EventType } from "@calcom/prisma/client";
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesService {
|
export class EventTypesService_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesRepository: EventTypesRepository,
|
private readonly eventTypesRepository: EventTypesRepository_2024_04_15,
|
||||||
private readonly membershipsRepository: MembershipsRepository,
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
private readonly dbWrite: PrismaWriteService
|
private readonly dbWrite: PrismaWriteService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput): Promise<EventTypeOutput> {
|
async createUserEventType(
|
||||||
|
user: UserWithProfile,
|
||||||
|
body: CreateEventTypeInput_2024_04_15
|
||||||
|
): Promise<EventTypeOutput> {
|
||||||
await this.checkCanCreateEventType(user.id, body);
|
await this.checkCanCreateEventType(user.id, body);
|
||||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||||
const { eventType } = await createEventType({
|
const { eventType } = await createEventType({
|
||||||
@@ -38,7 +41,7 @@ export class EventTypesService {
|
|||||||
return eventType as EventTypeOutput;
|
return eventType as EventTypeOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) {
|
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_04_15) {
|
||||||
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
|
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
|
||||||
if (existsWithSlug) {
|
if (existsWithSlug) {
|
||||||
throw new BadRequestException("User already has an event type with this slug.");
|
throw new BadRequestException("User already has an event type with this slug.");
|
||||||
@@ -114,7 +117,7 @@ export class EventTypesService {
|
|||||||
return defaultEventTypes;
|
return defaultEventTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
|
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
|
||||||
this.checkCanUpdateEventType(user.id, eventTypeId);
|
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||||
await updateEventType({
|
await updateEventType({
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export const DEFAULT_EVENT_TYPES = {
|
||||||
|
thirtyMinutes: { length: 30, slug: "thirty-minutes", title: "30 Minutes" },
|
||||||
|
thirtyMinutesVideo: {
|
||||||
|
length: 30,
|
||||||
|
slug: "thirty-minutes-video",
|
||||||
|
title: "30 Minutes",
|
||||||
|
locations: [{ type: "integrations:daily" }],
|
||||||
|
},
|
||||||
|
sixtyMinutes: { length: 60, slug: "sixty-minutes", title: "60 Minutes" },
|
||||||
|
sixtyMinutesVideo: {
|
||||||
|
length: 60,
|
||||||
|
slug: "sixty-minutes-video",
|
||||||
|
title: "60 Minutes",
|
||||||
|
locations: [{ type: "integrations:daily" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
+295
@@ -0,0 +1,295 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
ApiSuccessResponse,
|
||||||
|
CreateEventTypeInput_2024_06_14,
|
||||||
|
EventTypeOutput_2024_06_14,
|
||||||
|
UpdateEventTypeInput_2024_06_14,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
describe("Event types Endpoints", () => {
|
||||||
|
describe("Not authenticated", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||||
|
})
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/:id`, () => {
|
||||||
|
return request(app.getHttpServer()).get("/api/v2/event-types/100").expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("User Authenticated", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let organization: Team;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "event-types-test-e2e@api.com";
|
||||||
|
const name = "bob-the-builder";
|
||||||
|
const username = name;
|
||||||
|
let eventType: EventTypeOutput_2024_06_14;
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
name,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: ["redirect-uri"],
|
||||||
|
permissions: 32,
|
||||||
|
};
|
||||||
|
const secret = "secret";
|
||||||
|
|
||||||
|
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(oAuthClient).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create an event type", async () => {
|
||||||
|
const body: CreateEventTypeInput_2024_06_14 = {
|
||||||
|
title: "Coding class",
|
||||||
|
slug: "coding-class",
|
||||||
|
description: "Let's learn how to code like a pro.",
|
||||||
|
lengthInMinutes: 60,
|
||||||
|
locations: [
|
||||||
|
{
|
||||||
|
type: "integration",
|
||||||
|
integration: "cal-video",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookingFields: [
|
||||||
|
{
|
||||||
|
type: "select",
|
||||||
|
label: "select which language you want to learn",
|
||||||
|
slug: "select-language",
|
||||||
|
required: true,
|
||||||
|
placeholder: "select language",
|
||||||
|
options: ["javascript", "python", "cobol"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(body.title);
|
||||||
|
expect(createdEventType.description).toEqual(body.description);
|
||||||
|
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
||||||
|
expect(createdEventType.locations).toEqual(body.locations);
|
||||||
|
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
|
||||||
|
expect(createdEventType.ownerId).toEqual(user.id);
|
||||||
|
|
||||||
|
eventType = responseBody.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update event type", async () => {
|
||||||
|
const newTitle = "Coding class in Italian!";
|
||||||
|
|
||||||
|
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||||
|
title: newTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
expect(updatedEventType.title).toEqual(body.title);
|
||||||
|
|
||||||
|
expect(updatedEventType.id).toEqual(eventType.id);
|
||||||
|
expect(updatedEventType.title).toEqual(newTitle);
|
||||||
|
expect(updatedEventType.description).toEqual(eventType.description);
|
||||||
|
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||||
|
expect(updatedEventType.locations).toEqual(eventType.locations);
|
||||||
|
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||||
|
expect(updatedEventType.ownerId).toEqual(user.id);
|
||||||
|
|
||||||
|
eventType.title = newTitle;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/:id`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||||
|
.set("Authorization", `Bearer whatever`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const fetchedEventType = responseBody.data;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(fetchedEventType.id).toEqual(eventType.id);
|
||||||
|
expect(fetchedEventType.title).toEqual(eventType.title);
|
||||||
|
expect(fetchedEventType.description).toEqual(eventType.description);
|
||||||
|
expect(fetchedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||||
|
expect(fetchedEventType.locations).toEqual(eventType.locations);
|
||||||
|
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||||
|
expect(fetchedEventType.ownerId).toEqual(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/even-types by username`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types?username=${username}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||||
|
.set("Authorization", `Bearer whatever`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toEqual(1);
|
||||||
|
|
||||||
|
const fetchedEventType = responseBody.data?.[0];
|
||||||
|
|
||||||
|
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||||
|
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||||
|
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||||
|
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||||
|
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||||
|
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||||
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/event-types by username and eventSlug`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||||
|
.set("Authorization", `Bearer whatever`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||||
|
const fetchedEventType = responseBody.data[0];
|
||||||
|
|
||||||
|
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||||
|
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||||
|
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||||
|
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||||
|
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||||
|
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||||
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/:id not existing`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types/1000`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||||
|
.set("Authorization", `Bearer whatever`)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should delete schedule", async () => {
|
||||||
|
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
try {
|
||||||
|
await eventTypesRepositoryFixture.delete(eventType.id);
|
||||||
|
} catch (e) {
|
||||||
|
// Event type might have been deleted by the test
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await userRepositoryFixture.delete(user.id);
|
||||||
|
} catch (e) {
|
||||||
|
// User might have been deleted by the test
|
||||||
|
}
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
|
||||||
|
import { DeleteEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output";
|
||||||
|
import { GetEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output";
|
||||||
|
import { GetEventTypesOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output";
|
||||||
|
import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output";
|
||||||
|
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||||
|
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
UseGuards,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
NotFoundException,
|
||||||
|
Patch,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
CreateEventTypeInput_2024_06_14,
|
||||||
|
UpdateEventTypeInput_2024_06_14,
|
||||||
|
GetEventTypesQuery_2024_06_14,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/event-types",
|
||||||
|
version: VERSION_2024_06_14_VALUE,
|
||||||
|
})
|
||||||
|
@UseGuards(PermissionsGuard)
|
||||||
|
@DocsTags("Event types")
|
||||||
|
export class EventTypesController_2024_06_14 {
|
||||||
|
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
|
||||||
|
|
||||||
|
@Post("/")
|
||||||
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async createEventType(
|
||||||
|
@Body() body: CreateEventTypeInput_2024_06_14,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<CreateEventTypeOutput_2024_06_14> {
|
||||||
|
const eventType = await this.eventTypesService.createUserEventType(user, body);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:eventTypeId")
|
||||||
|
@Permissions([EVENT_TYPE_READ])
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async getEventTypeById(
|
||||||
|
@Param("eventTypeId") eventTypeId: string,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<GetEventTypeOutput_2024_06_14> {
|
||||||
|
const eventType = await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@Permissions([EVENT_TYPE_READ])
|
||||||
|
async getEventTypes(
|
||||||
|
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
||||||
|
): Promise<GetEventTypesOutput_2024_06_14> {
|
||||||
|
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventTypes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/:eventTypeId")
|
||||||
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async updateEventType(
|
||||||
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
|
@Body() body: UpdateEventTypeInput_2024_06_14,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<UpdateEventTypeOutput_2024_06_14> {
|
||||||
|
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("/:eventTypeId")
|
||||||
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async deleteEventType(
|
||||||
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
|
@GetUser("id") userId: number
|
||||||
|
): Promise<DeleteEventTypeOutput_2024_06_14> {
|
||||||
|
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: {
|
||||||
|
id: eventType.id,
|
||||||
|
lengthInMinutes: eventType.length,
|
||||||
|
slug: eventType.slug,
|
||||||
|
title: eventType.title,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
|
||||||
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
|
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||||
|
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||||
|
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
|
||||||
|
providers: [
|
||||||
|
EventTypesRepository_2024_06_14,
|
||||||
|
EventTypesService_2024_06_14,
|
||||||
|
InputEventTypesService_2024_06_14,
|
||||||
|
OutputEventTypesService_2024_06_14,
|
||||||
|
UsersRepository,
|
||||||
|
UsersService,
|
||||||
|
],
|
||||||
|
controllers: [EventTypesController_2024_06_14],
|
||||||
|
exports: [EventTypesService_2024_06_14, EventTypesRepository_2024_06_14],
|
||||||
|
})
|
||||||
|
export class EventTypesModule_2024_06_14 {}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getEventTypeById,
|
||||||
|
transformApiEventTypeBookingFields,
|
||||||
|
transformApiEventTypeLocations,
|
||||||
|
} from "@calcom/platform-libraries-0.0.13";
|
||||||
|
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
type InputEventTransformed = Omit<
|
||||||
|
CreateEventTypeInput_2024_06_14,
|
||||||
|
"lengthInMinutes" | "locations" | "bookingFields"
|
||||||
|
> & {
|
||||||
|
length: number;
|
||||||
|
slug: string;
|
||||||
|
locations?: ReturnType<typeof transformApiEventTypeLocations>;
|
||||||
|
bookingFields?: ReturnType<typeof transformApiEventTypeBookingFields>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EventTypesRepository_2024_06_14 {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async createUserEventType(userId: number, body: InputEventTransformed) {
|
||||||
|
return this.dbWrite.prisma.eventType.create({
|
||||||
|
data: {
|
||||||
|
...body,
|
||||||
|
userId,
|
||||||
|
locations: body.locations,
|
||||||
|
bookingFields: body.bookingFields,
|
||||||
|
users: { connect: { id: userId } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventTypeWithSeats(eventTypeId: number) {
|
||||||
|
return this.dbRead.prisma.eventType.findUnique({
|
||||||
|
where: { id: eventTypeId },
|
||||||
|
select: { users: { select: { id: true } }, seatsPerTimeSlot: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserEventType(userId: number, eventTypeId: number) {
|
||||||
|
return this.dbRead.prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: eventTypeId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
include: { users: true, schedule: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserEventTypes(userId: number) {
|
||||||
|
return this.dbRead.prisma.eventType.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
include: { users: true, schedule: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserEventTypeForAtom(
|
||||||
|
user: UserWithProfile,
|
||||||
|
isUserOrganizationAdmin: boolean,
|
||||||
|
eventTypeId: number
|
||||||
|
) {
|
||||||
|
return await getEventTypeById({
|
||||||
|
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
||||||
|
eventTypeId,
|
||||||
|
userId: user.id,
|
||||||
|
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||||
|
isUserOrganizationAdmin,
|
||||||
|
isTrpcCall: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventTypeById(eventTypeId: number) {
|
||||||
|
return this.dbRead.prisma.eventType.findUnique({
|
||||||
|
where: { id: eventTypeId },
|
||||||
|
include: { users: true, schedule: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserEventTypeBySlug(userId: number, slug: string) {
|
||||||
|
return this.dbRead.prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_slug: {
|
||||||
|
userId: userId,
|
||||||
|
slug: slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { users: true, schedule: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEventType(eventTypeId: number) {
|
||||||
|
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsIn, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export class CreateEventTypeOutput_2024_06_14 {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: EventTypeOutput_2024_06_14,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => EventTypeOutput_2024_06_14)
|
||||||
|
data!: EventTypeOutput_2024_06_14;
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsIn, IsInt, IsString } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
class DeleteData_2024_06_14 {
|
||||||
|
@IsInt()
|
||||||
|
@DocsProperty({ example: 1 })
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||||
|
lengthInMinutes!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
slug!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||||
|
title!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeleteEventTypeOutput_2024_06_14 {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@Type(() => DeleteData_2024_06_14)
|
||||||
|
data!: DeleteData_2024_06_14;
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsIn, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export class GetEventTypeOutput_2024_06_14 {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: EventTypeOutput_2024_06_14,
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => EventTypeOutput_2024_06_14)
|
||||||
|
data!: EventTypeOutput_2024_06_14 | null;
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsArray, IsIn, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export class GetEventTypesOutput_2024_06_14 {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EventTypeOutput_2024_06_14)
|
||||||
|
@IsArray()
|
||||||
|
data!: EventTypeOutput_2024_06_14[];
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsIn, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export class UpdateEventTypeOutput_2024_06_14 {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: EventTypeOutput_2024_06_14,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => EventTypeOutput_2024_06_14)
|
||||||
|
data!: EventTypeOutput_2024_06_14;
|
||||||
|
}
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
|
||||||
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
|
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||||
|
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||||
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||||
|
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||||
|
import { dynamicEvent } from "@calcom/platform-libraries-0.0.13";
|
||||||
|
import {
|
||||||
|
CreateEventTypeInput_2024_06_14,
|
||||||
|
UpdateEventTypeInput_2024_06_14,
|
||||||
|
GetEventTypesQuery_2024_06_14,
|
||||||
|
EventTypeOutput_2024_06_14,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EventTypesService_2024_06_14 {
|
||||||
|
constructor(
|
||||||
|
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||||
|
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
|
||||||
|
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||||
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
|
private readonly usersRepository: UsersRepository,
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
|
private readonly dbWrite: PrismaWriteService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
|
||||||
|
await this.checkCanCreateEventType(user.id, body);
|
||||||
|
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||||
|
const bodyTransformed = this.inputEventTypesService.transformInputCreateEventType(body);
|
||||||
|
const { eventType: eventTypeCreated } = await createEventType({
|
||||||
|
input: bodyTransformed,
|
||||||
|
ctx: {
|
||||||
|
user: eventTypeUser,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
prisma: this.dbWrite.prisma,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id);
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
throw new NotFoundException(`Event type with id ${eventTypeCreated.id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_06_14) {
|
||||||
|
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
|
||||||
|
if (existsWithSlug) {
|
||||||
|
throw new BadRequestException("User already has an event type with this slug.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) {
|
||||||
|
const user = await this.usersRepository.findByUsername(username);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, eventTypeSlug);
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventTypesByUsername(username: string) {
|
||||||
|
const user = await this.usersRepository.findByUsername(username);
|
||||||
|
if (!user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.getUserEventTypes(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserToCreateEvent(user: UserWithProfile) {
|
||||||
|
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
||||||
|
const isOrgAdmin = organizationId
|
||||||
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
|
: false;
|
||||||
|
const profileId = user.movedToProfile?.id || null;
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
organizationId: user.organizationId,
|
||||||
|
organization: { isOrgAdmin },
|
||||||
|
profile: { id: profileId },
|
||||||
|
metadata: user.metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserEventType(userId: number, eventTypeId: number) {
|
||||||
|
const eventType = await this.eventTypesRepository.getUserEventType(userId, eventTypeId);
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsEventType(userId, eventType);
|
||||||
|
return this.outputEventTypesService.getResponseEventType(userId, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserEventTypes(userId: number) {
|
||||||
|
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId);
|
||||||
|
|
||||||
|
const eventTypePromises = eventTypes.map(async (eventType) => {
|
||||||
|
return await this.outputEventTypesService.getResponseEventType(userId, eventType);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(eventTypePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||||
|
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
||||||
|
|
||||||
|
const isUserOrganizationAdmin = organizationId
|
||||||
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const eventType = await this.eventTypesRepository.getUserEventTypeForAtom(
|
||||||
|
user,
|
||||||
|
isUserOrganizationAdmin,
|
||||||
|
eventTypeId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
||||||
|
const user = await this.usersRepository.findByUsername(username);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with username "${username}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getEventTypesPublic(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14): Promise<EventTypeOutput_2024_06_14[]> {
|
||||||
|
const { username, eventSlug, usernames } = queryParams;
|
||||||
|
|
||||||
|
if (username && eventSlug) {
|
||||||
|
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug);
|
||||||
|
return eventType ? [eventType] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
return await this.getEventTypesByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernames) {
|
||||||
|
const dynamicEventType = await this.getDynamicEventType(usernames);
|
||||||
|
return [dynamicEventType];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDynamicEventType(usernames: string[]) {
|
||||||
|
const users = await this.usersService.getByUsernames(usernames);
|
||||||
|
const usersFiltered: UserWithProfile[] = [];
|
||||||
|
for (const user of users) {
|
||||||
|
if (user) {
|
||||||
|
usersFiltered.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.outputEventTypesService.getResponseEventType(0, {
|
||||||
|
...dynamicEvent,
|
||||||
|
users: usersFiltered,
|
||||||
|
isInstantEvent: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUserDefaultEventTypes(userId: number) {
|
||||||
|
const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES;
|
||||||
|
|
||||||
|
const defaultEventTypes = await Promise.all([
|
||||||
|
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
|
||||||
|
this.eventTypesRepository.createUserEventType(userId, sixtyMinutes),
|
||||||
|
this.eventTypesRepository.createUserEventType(userId, thirtyMinutesVideo),
|
||||||
|
this.eventTypesRepository.createUserEventType(userId, sixtyMinutesVideo),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return defaultEventTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_06_14, user: UserWithProfile) {
|
||||||
|
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||||
|
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||||
|
const bodyTransformed = this.inputEventTypesService.transformInputUpdateEventType(body);
|
||||||
|
await updateEventType({
|
||||||
|
input: { id: eventTypeId, ...bodyTransformed },
|
||||||
|
ctx: {
|
||||||
|
user: eventTypeUser,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
prisma: this.dbWrite.prisma,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkCanUpdateEventType(userId: number, eventTypeId: number) {
|
||||||
|
const existingEventType = await this.getUserEventType(userId, eventTypeId);
|
||||||
|
if (!existingEventType) {
|
||||||
|
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||||
|
}
|
||||||
|
this.checkUserOwnsEventType(userId, { id: eventTypeId, userId: existingEventType.ownerId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||||
|
const profileId = user.movedToProfile?.id || null;
|
||||||
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
|
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEventType(eventTypeId: number, userId: number) {
|
||||||
|
const existingEventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||||
|
if (!existingEventType) {
|
||||||
|
throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsEventType(userId, existingEventType);
|
||||||
|
|
||||||
|
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserOwnsEventType(userId: number, eventType: Pick<EventType, "id" | "userId">) {
|
||||||
|
if (userId !== eventType.userId) {
|
||||||
|
throw new ForbiddenException(`User with ID=${userId} does not own event type with ID=${eventType.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
transformApiEventTypeBookingFields,
|
||||||
|
transformApiEventTypeLocations,
|
||||||
|
} from "@calcom/platform-libraries-0.0.13";
|
||||||
|
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InputEventTypesService_2024_06_14 {
|
||||||
|
transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) {
|
||||||
|
const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [
|
||||||
|
{
|
||||||
|
type: "integration",
|
||||||
|
integration: "cal-video",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { lengthInMinutes, locations, bookingFields, ...rest } = inputEventType;
|
||||||
|
|
||||||
|
const eventType = {
|
||||||
|
...rest,
|
||||||
|
length: lengthInMinutes,
|
||||||
|
locations: this.transformInputLocations(locations || defaultLocations),
|
||||||
|
bookingFields: this.transformInputBookingFields(bookingFields),
|
||||||
|
};
|
||||||
|
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
|
||||||
|
const { locations, bookingFields, ...rest } = inputEventType;
|
||||||
|
|
||||||
|
const eventType = {
|
||||||
|
...rest,
|
||||||
|
locations: locations ? this.transformInputLocations(locations) : undefined,
|
||||||
|
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
|
||||||
|
return transformApiEventTypeLocations(inputLocations);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
|
||||||
|
return transformApiEventTypeBookingFields(inputBookingFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import type { EventType, User, Schedule } from "@prisma/client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EventTypeMetaDataSchema,
|
||||||
|
userMetadata,
|
||||||
|
getResponseEventTypeLocations,
|
||||||
|
getResponseEventTypeBookingFields,
|
||||||
|
parseRecurringEvent,
|
||||||
|
TransformedLocationsSchema,
|
||||||
|
BookingFieldsSchema,
|
||||||
|
} from "@calcom/platform-libraries-0.0.13";
|
||||||
|
|
||||||
|
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
|
||||||
|
type DatabaseEventType = EventType & EventTypeRelations;
|
||||||
|
|
||||||
|
type Input = Pick<
|
||||||
|
DatabaseEventType,
|
||||||
|
| "id"
|
||||||
|
| "length"
|
||||||
|
| "title"
|
||||||
|
| "description"
|
||||||
|
| "disableGuests"
|
||||||
|
| "slotInterval"
|
||||||
|
| "minimumBookingNotice"
|
||||||
|
| "beforeEventBuffer"
|
||||||
|
| "afterEventBuffer"
|
||||||
|
| "slug"
|
||||||
|
| "schedulingType"
|
||||||
|
| "requiresConfirmation"
|
||||||
|
| "price"
|
||||||
|
| "currency"
|
||||||
|
| "lockTimeZoneToggleOnBookingPage"
|
||||||
|
| "seatsPerTimeSlot"
|
||||||
|
| "forwardParamsSuccessRedirect"
|
||||||
|
| "successRedirectUrl"
|
||||||
|
| "seatsShowAvailabilityCount"
|
||||||
|
| "isInstantEvent"
|
||||||
|
| "locations"
|
||||||
|
| "bookingFields"
|
||||||
|
| "recurringEvent"
|
||||||
|
| "metadata"
|
||||||
|
| "users"
|
||||||
|
| "schedule"
|
||||||
|
>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OutputEventTypesService_2024_06_14 {
|
||||||
|
async getResponseEventType(ownerId: number, databaseEventType: Input) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
length,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
disableGuests,
|
||||||
|
slotInterval,
|
||||||
|
minimumBookingNotice,
|
||||||
|
beforeEventBuffer,
|
||||||
|
afterEventBuffer,
|
||||||
|
slug,
|
||||||
|
schedulingType,
|
||||||
|
requiresConfirmation,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
|
lockTimeZoneToggleOnBookingPage,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
forwardParamsSuccessRedirect,
|
||||||
|
successRedirectUrl,
|
||||||
|
seatsShowAvailabilityCount,
|
||||||
|
isInstantEvent,
|
||||||
|
} = databaseEventType;
|
||||||
|
|
||||||
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
|
const bookingFields = this.transformBookingFields(databaseEventType.bookingFields);
|
||||||
|
const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||||
|
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||||
|
const schedule = await this.getSchedule(databaseEventType);
|
||||||
|
const users = this.transformUsers(databaseEventType.users);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
ownerId,
|
||||||
|
lengthInMinutes: length,
|
||||||
|
title,
|
||||||
|
slug,
|
||||||
|
description: description || "",
|
||||||
|
locations,
|
||||||
|
bookingFields,
|
||||||
|
recurringEvent,
|
||||||
|
disableGuests,
|
||||||
|
slotInterval,
|
||||||
|
minimumBookingNotice,
|
||||||
|
beforeEventBuffer,
|
||||||
|
afterEventBuffer,
|
||||||
|
schedulingType,
|
||||||
|
metadata,
|
||||||
|
requiresConfirmation,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
|
lockTimeZoneToggleOnBookingPage,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
forwardParamsSuccessRedirect,
|
||||||
|
successRedirectUrl,
|
||||||
|
seatsShowAvailabilityCount,
|
||||||
|
isInstantEvent,
|
||||||
|
users,
|
||||||
|
schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
transformLocations(locations: any) {
|
||||||
|
if (!locations) return [];
|
||||||
|
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
|
||||||
|
}
|
||||||
|
|
||||||
|
transformBookingFields(inputBookingFields: any) {
|
||||||
|
if (!inputBookingFields) return [];
|
||||||
|
return getResponseEventTypeBookingFields(BookingFieldsSchema.parse(inputBookingFields));
|
||||||
|
}
|
||||||
|
|
||||||
|
transformRecurringEvent(recurringEvent: any) {
|
||||||
|
if (!recurringEvent) return null;
|
||||||
|
return parseRecurringEvent(recurringEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformMetadata(metadata: any) {
|
||||||
|
if (!metadata) return {};
|
||||||
|
return EventTypeMetaDataSchema.parse(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSchedule(databaseEventType: Input) {
|
||||||
|
return databaseEventType.schedule || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformUsers(users: User[]) {
|
||||||
|
return users.map((user) => {
|
||||||
|
const metadata = user.metadata ? userMetadata.parse(user.metadata) : {};
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
brandColor: user.brandColor,
|
||||||
|
darkBrandColor: user.darkBrandColor,
|
||||||
|
weekStart: user.weekStart,
|
||||||
|
metadata: metadata || {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
|||||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
@@ -61,7 +61,7 @@ export class GcalController {
|
|||||||
|
|
||||||
@Get("/oauth/auth-url")
|
@Get("/oauth/auth-url")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async redirect(
|
async redirect(
|
||||||
@Headers("Authorization") authorization: string,
|
@Headers("Authorization") authorization: string,
|
||||||
@Req() req: Request
|
@Req() req: Request
|
||||||
@@ -90,7 +90,7 @@ export class GcalController {
|
|||||||
|
|
||||||
@Get("/check")
|
@Get("/check")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@Permissions([APPS_READ])
|
@Permissions([APPS_READ])
|
||||||
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
|
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
|
||||||
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { User } from "@prisma/client";
|
|||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { UserResponse } from "@calcom/platform-types";
|
import { UserResponse } from "@calcom/platform-types";
|
||||||
@@ -30,7 +30,7 @@ describe("Me Endpoints", () => {
|
|||||||
let user: User;
|
let user: User;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withApiAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
|
|||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
@@ -18,7 +18,7 @@ import { userSchemaResponse } from "@calcom/platform-types";
|
|||||||
path: "/v2/me",
|
path: "/v2/me",
|
||||||
version: API_VERSIONS_VALUES,
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@DocsTags("Me")
|
@DocsTags("Me")
|
||||||
export class MeController {
|
export class MeController {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BookingsModule } from "@/ee/bookings/bookings.module";
|
import { BookingsModule } from "@/ee/bookings/bookings.module";
|
||||||
import { CalendarsModule } from "@/ee/calendars/calendars.module";
|
import { CalendarsModule } from "@/ee/calendars/calendars.module";
|
||||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||||
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { GcalModule } from "@/ee/gcal/gcal.module";
|
import { GcalModule } from "@/ee/gcal/gcal.module";
|
||||||
import { MeModule } from "@/ee/me/me.module";
|
import { MeModule } from "@/ee/me/me.module";
|
||||||
import { ProviderModule } from "@/ee/provider/provider.module";
|
import { ProviderModule } from "@/ee/provider/provider.module";
|
||||||
@@ -17,7 +18,8 @@ import { Module } from "@nestjs/common";
|
|||||||
SchedulesModule_2024_04_15,
|
SchedulesModule_2024_04_15,
|
||||||
SchedulesModule_2024_06_11,
|
SchedulesModule_2024_06_11,
|
||||||
MeModule,
|
MeModule,
|
||||||
EventTypesModule,
|
EventTypesModule_2024_04_15,
|
||||||
|
EventTypesModule_2024_06_14,
|
||||||
CalendarsModule,
|
CalendarsModule,
|
||||||
BookingsModule,
|
BookingsModule,
|
||||||
SlotsModule,
|
SlotsModule,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-ac
|
|||||||
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
|
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
|
||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +45,7 @@ export class CalProviderController {
|
|||||||
|
|
||||||
@Get("/:clientId/access-token")
|
@Get("/:clientId/access-token")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async verifyAccessToken(
|
async verifyAccessToken(
|
||||||
@Param("clientId") clientId: string,
|
@Param("clientId") clientId: string,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@ import { User } from "@prisma/client";
|
|||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
|
||||||
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
@@ -37,7 +37,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
|
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withApiAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
|
|||||||
import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions";
|
import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -35,7 +35,7 @@ import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input"
|
|||||||
path: "/v2/schedules",
|
path: "/v2/schedules",
|
||||||
version: VERSION_2024_04_15_VALUE,
|
version: VERSION_2024_04_15_VALUE,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@DocsTags("Schedules")
|
@DocsTags("Schedules")
|
||||||
export class SchedulesController_2024_04_15 {
|
export class SchedulesController_2024_04_15 {
|
||||||
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
|
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import { User } from "@prisma/client";
|
|||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_11 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_11 } from "@calcom/platform-constants";
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +52,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
let createdSchedule: CreateScheduleOutput_2024_06_11["data"];
|
let createdSchedule: CreateScheduleOutput_2024_06_11["data"];
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withApiAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_06_11],
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_06_11],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||||
import { VERSION_2024_06_11_VALUE } from "@/lib/api-versions";
|
import { VERSION_2024_06_14, VERSION_2024_06_11 } from "@/lib/api-versions";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -34,9 +34,9 @@ import {
|
|||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/schedules",
|
path: "/v2/schedules",
|
||||||
version: VERSION_2024_06_11_VALUE,
|
version: [VERSION_2024_06_14, VERSION_2024_06_11],
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@DocsTags("Schedules")
|
@DocsTags("Schedules")
|
||||||
export class SchedulesController_2024_06_11 {
|
export class SchedulesController_2024_06_11 {
|
||||||
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
|
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export type Environment = {
|
|||||||
STRIPE_API_KEY: string;
|
STRIPE_API_KEY: string;
|
||||||
STRIPE_WEBHOOK_SECRET: string;
|
STRIPE_WEBHOOK_SECRET: string;
|
||||||
WEB_APP_URL: string;
|
WEB_APP_URL: string;
|
||||||
|
IS_E2E: boolean;
|
||||||
|
CALCOM_LICENSE_KEY: string;
|
||||||
|
GET_LICENSE_KEY_URL: string;
|
||||||
|
API_KEY_PREFIX: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
|
||||||
export const hashAPIKey = (apiKey: string): string => createHash("sha256").update(apiKey).digest("hex");
|
export const hashAPIKey = (apiKey: string): string => createHash("sha256").update(apiKey).digest("hex");
|
||||||
|
|
||||||
|
export const isApiKey = (authString: string, prefix: string): boolean =>
|
||||||
|
authString?.startsWith(prefix ?? "cal_");
|
||||||
|
|
||||||
|
export const stripApiKey = (apiKey: string, prefix?: string): string => apiKey.replace(prefix ?? "cal_", "");
|
||||||
@@ -3,11 +3,15 @@ import { VersionValue } from "@nestjs/common/interfaces";
|
|||||||
import {
|
import {
|
||||||
API_VERSIONS,
|
API_VERSIONS,
|
||||||
VERSION_2024_04_15,
|
VERSION_2024_04_15,
|
||||||
VERSION_2024_05_21,
|
|
||||||
VERSION_2024_06_11,
|
VERSION_2024_06_11,
|
||||||
|
VERSION_2024_06_14,
|
||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
|
|
||||||
export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue;
|
export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue;
|
||||||
|
export const VERSION_2024_06_14_VALUE: VersionValue = VERSION_2024_06_14 as unknown as VersionValue;
|
||||||
export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue;
|
export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue;
|
||||||
export const VERSION_2024_05_21_VALUE: VersionValue = VERSION_2024_05_21 as unknown as VersionValue;
|
|
||||||
export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue;
|
export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue;
|
||||||
|
|
||||||
|
export { VERSION_2024_04_15 };
|
||||||
|
export { VERSION_2024_06_11 };
|
||||||
|
export { VERSION_2024_06_14 };
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getApiKeyFromHash(hashedKey: string) {
|
||||||
|
return this.dbRead.prisma.apiKey.findUnique({
|
||||||
|
where: {
|
||||||
|
hashedKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ApiKeyService } from "@/modules/api-key/api-key.service";
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
providers: [ApiKeyService],
|
providers: [ApiKeyRepository],
|
||||||
exports: [ApiKeyService],
|
exports: [ApiKeyRepository],
|
||||||
})
|
})
|
||||||
export class ApiKeyModule {}
|
export class ApiKeyModule {}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { hashAPIKey } from "@/lib/api-key";
|
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import type { Request } from "express";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ApiKeyService {
|
|
||||||
constructor(private readonly dbRead: PrismaReadService) {}
|
|
||||||
|
|
||||||
async retrieveApiKey(request: Request) {
|
|
||||||
const apiKey = request.get("Authorization")?.replace("Bearer ", "");
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedKey = hashAPIKey(apiKey.replace("cal_", ""));
|
|
||||||
|
|
||||||
return this.dbRead.prisma.apiKey.findUniqueOrThrow({
|
|
||||||
where: {
|
|
||||||
hashedKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||||
import { AccessTokenStrategy } from "@/modules/auth/strategies/access-token/access-token.strategy";
|
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||||
import { ApiKeyAuthStrategy } from "@/modules/auth/strategies/api-key-auth/api-key-auth.strategy";
|
|
||||||
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
|
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
|
||||||
|
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { RedisModule } from "@/modules/redis/redis.module";
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
@@ -13,15 +13,16 @@ import { Module } from "@nestjs/common";
|
|||||||
import { PassportModule } from "@nestjs/passport";
|
import { PassportModule } from "@nestjs/passport";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PassportModule, RedisModule, ApiKeyModule, UsersModule, MembershipsModule, TokensModule],
|
imports: [
|
||||||
providers: [
|
PassportModule,
|
||||||
ApiKeyAuthStrategy,
|
RedisModule,
|
||||||
NextAuthGuard,
|
ApiKeyModule,
|
||||||
NextAuthStrategy,
|
UsersModule,
|
||||||
AccessTokenGuard,
|
MembershipsModule,
|
||||||
AccessTokenStrategy,
|
TokensModule,
|
||||||
OAuthFlowService,
|
DeploymentsModule,
|
||||||
],
|
],
|
||||||
exports: [NextAuthGuard, AccessTokenGuard],
|
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||||
|
exports: [NextAuthGuard, ApiAuthGuard],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
|
import { createParamDecorator } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
export type GetOrgReturnType = Team;
|
||||||
|
|
||||||
|
export const GetOrg = createParamDecorator<
|
||||||
|
keyof GetOrgReturnType | (keyof GetOrgReturnType)[],
|
||||||
|
ExecutionContext
|
||||||
|
>((data, ctx) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
const organization = request.organization as GetOrgReturnType;
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
throw new Error("GetOrg decorator : Org not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.reduce((prev, curr) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[curr]: organization[curr],
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return organization[data];
|
||||||
|
}
|
||||||
|
|
||||||
|
return organization;
|
||||||
|
});
|
||||||
@@ -1,26 +1,33 @@
|
|||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { ExecutionContext } from "@nestjs/common";
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
import { createParamDecorator } from "@nestjs/common";
|
import { createParamDecorator } from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
|
||||||
|
|
||||||
export const GetUser = createParamDecorator<keyof User | (keyof User)[], ExecutionContext>((data, ctx) => {
|
export type GetUserReturnType = UserWithProfile & { isSystemAdmin: boolean };
|
||||||
|
|
||||||
|
export const GetUser = createParamDecorator<
|
||||||
|
keyof GetUserReturnType | (keyof GetUserReturnType)[],
|
||||||
|
ExecutionContext
|
||||||
|
>((data, ctx) => {
|
||||||
const request = ctx.switchToHttp().getRequest();
|
const request = ctx.switchToHttp().getRequest();
|
||||||
const user = request.user as User;
|
const user = request.user as GetUserReturnType;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("GetUser decorator : User not found");
|
throw new Error("GetUser decorator : User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.isSystemAdmin = user.role === "ADMIN";
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data.reduce((prev, curr) => {
|
return data.reduce((prev, curr) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[curr]: request.user[curr],
|
[curr]: user[curr],
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
return request.user[data];
|
return user[data];
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { AuthGuard } from "@nestjs/passport";
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
|
||||||
export class AccessTokenGuard extends AuthGuard("access-token") {
|
export class ApiAuthGuard extends AuthGuard("api-auth") {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,28 @@
|
|||||||
|
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||||
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IsOrgGuard implements CanActivate {
|
||||||
|
constructor(private organizationsRepository: OrganizationsRepository) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
|
||||||
|
const organizationId: string = request.params.orgId;
|
||||||
|
|
||||||
|
if (!organizationId) {
|
||||||
|
throw new ForbiddenException("No organization id found in request params.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const org = await this.organizationsRepository.findById(Number(organizationId));
|
||||||
|
|
||||||
|
if (org && org.isOrganization) {
|
||||||
|
request.organization = org;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { createMock } from "@golevelup/ts-jest";
|
import { createMock } from "@golevelup/ts-jest";
|
||||||
import { ExecutionContext } from "@nestjs/common";
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
import { APPS_WRITE, SCHEDULE_READ, SCHEDULE_WRITE } from "@calcom/platform-constants";
|
import { APPS_WRITE, SCHEDULE_READ, SCHEDULE_WRITE } from "@calcom/platform-constants";
|
||||||
@@ -13,7 +14,7 @@ describe("PermissionsGuard", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
reflector = new Reflector();
|
reflector = new Reflector();
|
||||||
guard = new PermissionsGuard(reflector, createMock<TokensRepository>());
|
guard = new PermissionsGuard(reflector, createMock<TokensRepository>(), createMock<ConfigService>());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be defined", () => {
|
it("should be defined", () => {
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { isApiKey } from "@/lib/api-key";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
import { hasPermissions } from "@calcom/platform-utils";
|
import { hasPermissions } from "@calcom/platform-utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PermissionsGuard implements CanActivate {
|
export class PermissionsGuard implements CanActivate {
|
||||||
constructor(private reflector: Reflector, private tokensRepository: TokensRepository) {}
|
constructor(
|
||||||
|
private reflector: Reflector,
|
||||||
|
private tokensRepository: TokensRepository,
|
||||||
|
private readonly config: ConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const requiredPermissions = this.reflector.get(Permissions, context.getHandler());
|
const requiredPermissions = this.reflector.get(Permissions, context.getHandler());
|
||||||
@@ -17,13 +23,18 @@ export class PermissionsGuard implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const accessToken = request.get("Authorization")?.replace("Bearer ", "");
|
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!authString) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oAuthClientPermissions = await this.getOAuthClientPermissions(accessToken);
|
// only check permissions for accessTokens attached to an oAuth Client
|
||||||
|
if (isApiKey(authString, this.config.get("api.apiKeyPrefix") ?? "cal_")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oAuthClientPermissions = await this.getOAuthClientPermissions(authString);
|
||||||
|
|
||||||
if (!oAuthClientPermissions) {
|
if (!oAuthClientPermissions) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
|
||||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import type { Request } from "express";
|
|
||||||
|
|
||||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AccessTokenStrategy extends PassportStrategy(BaseStrategy, "access-token") {
|
|
||||||
constructor(
|
|
||||||
private readonly oauthFlowService: OAuthFlowService,
|
|
||||||
private readonly tokensRepository: TokensRepository,
|
|
||||||
private readonly userRepository: UsersRepository
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async authenticate(request: Request) {
|
|
||||||
try {
|
|
||||||
const accessToken = request.get("Authorization")?.replace("Bearer ", "");
|
|
||||||
const requestOrigin = request.get("Origin");
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
|
||||||
if (!accessTokenValid) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await this.tokensRepository.getAccessTokenClient(accessToken);
|
|
||||||
if (!client) {
|
|
||||||
throw new UnauthorizedException("OAuth client not found given the access token");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestOrigin && !client.redirectUris.some((uri) => uri.startsWith(requestOrigin))) {
|
|
||||||
throw new UnauthorizedException(
|
|
||||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${requestOrigin}' to the 'Redirect uris' of your OAuth client.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
|
||||||
|
|
||||||
if (!ownerId) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.success(user);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) return this.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import appConfig from "@/config/app";
|
||||||
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
|
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||||
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
|
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||||
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { ExecutionContext, HttpException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
import { JwtService as NestJwtService } from "@nestjs/jwt";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { MockedRedisService } from "test/mocks/mock-redis-service";
|
||||||
|
|
||||||
|
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||||
|
|
||||||
|
describe("ApiAuthStrategy", () => {
|
||||||
|
let strategy: ApiAuthStrategy;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let organization: Team;
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
const validApiKeyEmail = "api-key-user-email@example.com";
|
||||||
|
const validAccessTokenEmail = "access-token-user-email@example.com";
|
||||||
|
let validApiKeyUser: User;
|
||||||
|
let validAccessTokenUser: User;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
ignoreEnvFile: true,
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfig],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
MockedRedisService,
|
||||||
|
ApiAuthStrategy,
|
||||||
|
ConfigService,
|
||||||
|
OAuthFlowService,
|
||||||
|
UsersRepository,
|
||||||
|
ApiKeyRepository,
|
||||||
|
DeploymentsService,
|
||||||
|
OAuthClientRepository,
|
||||||
|
PrismaReadService,
|
||||||
|
PrismaWriteService,
|
||||||
|
TokensRepository,
|
||||||
|
JwtService,
|
||||||
|
DeploymentsRepository,
|
||||||
|
NestJwtService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
strategy = module.get<ApiAuthStrategy>(ApiAuthStrategy);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(module);
|
||||||
|
tokensRepositoryFixture = new TokensRepositoryFixture(module);
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(module);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||||
|
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||||
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
|
validApiKeyUser = await userRepositoryFixture.create({
|
||||||
|
email: validApiKeyEmail,
|
||||||
|
});
|
||||||
|
validAccessTokenUser = await userRepositoryFixture.create({
|
||||||
|
email: validAccessTokenEmail,
|
||||||
|
});
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: ["http://localhost:3000"],
|
||||||
|
permissions: 32,
|
||||||
|
};
|
||||||
|
oAuthClient = await oAuthClientRepositoryFixture.create(organization.id, data, "secret");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authenticate with strategy", () => {
|
||||||
|
it("should return user associated with valid access token", async () => {
|
||||||
|
console.log("HERERERER");
|
||||||
|
const { accessToken } = await tokensRepositoryFixture.createTokens(
|
||||||
|
validAccessTokenUser.id,
|
||||||
|
oAuthClient.id
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Access token", accessToken);
|
||||||
|
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
get: (key: string) =>
|
||||||
|
({ Authorization: `Bearer ${accessToken}`, origin: "http://localhost:3000" }[key]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as ExecutionContext;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const user = await strategy.authenticate(request);
|
||||||
|
|
||||||
|
await expect(user.id).toEqual(validAccessTokenUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return user associated with valid api key", async () => {
|
||||||
|
const now = new Date();
|
||||||
|
now.setDate(now.getDate() + 1);
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(validApiKeyUser.id, now);
|
||||||
|
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer cal_test_${keyString}`,
|
||||||
|
},
|
||||||
|
get: (key: string) =>
|
||||||
|
({ Authorization: `Bearer cal_test_${keyString}`, origin: "http://localhost:3000" }[key]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as ExecutionContext;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const user = await strategy.authenticate(request);
|
||||||
|
|
||||||
|
expect(user.id).toEqual(validApiKeyUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw 401 if api key is invalid", async () => {
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer cal_test_}`,
|
||||||
|
},
|
||||||
|
get: (key: string) =>
|
||||||
|
({ Authorization: `Bearer cal_test_badkey1234`, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw 401 if Authorization header does not contain auth token", async () => {
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
get: (key: string) => ({ Authorization: ``, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
|
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||||
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
|
import type { Request } from "express";
|
||||||
|
|
||||||
|
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||||
|
constructor(
|
||||||
|
private readonly deploymentsService: DeploymentsService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly oauthFlowService: OAuthFlowService,
|
||||||
|
private readonly tokensRepository: TokensRepository,
|
||||||
|
private readonly userRepository: UsersRepository,
|
||||||
|
private readonly apiKeyRepository: ApiKeyRepository
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticate(request: Request) {
|
||||||
|
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||||
|
if (!authString) {
|
||||||
|
return this.error(new UnauthorizedException("No Authorization header provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOrigin = request.get("Origin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = isApiKey(authString, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||||
|
? await this.apiKeyStrategy(authString)
|
||||||
|
: await this.accessTokenStrategy(authString, requestOrigin);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return this.error(new UnauthorizedException("No user associated with the provided token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.success(user);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return this.error(err);
|
||||||
|
}
|
||||||
|
return this.error(
|
||||||
|
new InternalServerErrorException("An error occurred while authenticating the request")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiKeyStrategy(apiKey: string) {
|
||||||
|
const isLicenseValid = await this.deploymentsService.checkLicense();
|
||||||
|
if (!isLicenseValid) {
|
||||||
|
throw new UnauthorizedException("Invalid or missing CALCOM_LICENSE_KEY environment variable");
|
||||||
|
}
|
||||||
|
const strippedApiKey = stripApiKey(apiKey, this.config.get<string>("api.keyPrefix"));
|
||||||
|
const apiKeyHash = hashAPIKey(strippedApiKey);
|
||||||
|
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||||
|
if (!keyData) {
|
||||||
|
throw new UnauthorizedException("Your api key is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isKeyExpired =
|
||||||
|
keyData.expiresAt && new Date().setHours(0, 0, 0, 0) > keyData.expiresAt.setHours(0, 0, 0, 0);
|
||||||
|
if (isKeyExpired) {
|
||||||
|
throw new UnauthorizedException("Your api key is expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyOwnerId = keyData.userId;
|
||||||
|
if (!apiKeyOwnerId) {
|
||||||
|
throw new UnauthorizedException("No user tied to this apiKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(apiKeyOwnerId);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async accessTokenStrategy(accessToken: string, origin?: string) {
|
||||||
|
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
||||||
|
if (!accessTokenValid) {
|
||||||
|
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await this.tokensRepository.getAccessTokenClient(accessToken);
|
||||||
|
if (!client) {
|
||||||
|
throw new UnauthorizedException("OAuth client not found given the access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origin && !client.redirectUris.some((uri) => uri.startsWith(origin))) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||||
|
|
||||||
|
if (!ownerId) {
|
||||||
|
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
|
||||||
import { ApiKeyService } from "@/modules/api-key/api-key.service";
|
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
|
||||||
import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import type { Request } from "express";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ApiKeyAuthStrategy extends PassportStrategy(BaseStrategy, "api-key") {
|
|
||||||
constructor(
|
|
||||||
private readonly apiKeyService: ApiKeyService,
|
|
||||||
private readonly userRepository: UsersRepository
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async authenticate(req: Request) {
|
|
||||||
try {
|
|
||||||
const apiKey = await this.apiKeyService.retrieveApiKey(req);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new UnauthorizedException("Authorization token is missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apiKey.expiresAt && new Date() > apiKey.expiresAt) {
|
|
||||||
throw new UnauthorizedException("The API key is expired.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.userRepository.findById(apiKey.userId);
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException("User not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.success(user);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) return this.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
@@ -33,6 +33,9 @@ export class NextAuthStrategy extends PassportStrategy(NextAuthPassportStrategy,
|
|||||||
return this.success(user);
|
return this.success(user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) return this.error(error);
|
if (error instanceof Error) return this.error(error);
|
||||||
|
return this.error(
|
||||||
|
new InternalServerErrorException("An error occurred while authenticating the request")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { DeploymentsRepository } from "./deployments.repository";
|
||||||
|
import { DeploymentsService } from "./deployments.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [DeploymentsRepository, DeploymentsService, RedisService],
|
||||||
|
exports: [DeploymentsRepository, DeploymentsService],
|
||||||
|
})
|
||||||
|
export class DeploymentsModule {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeploymentsRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getDeployment() {
|
||||||
|
return this.dbRead.prisma.deployment.findFirst({ where: { id: 1 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
const CACHING_TIME = 86400000; // 24 hours in milliseconds
|
||||||
|
|
||||||
|
const getLicenseCacheKey = (key: string) => `api-v2-license-key-url-${key}`;
|
||||||
|
|
||||||
|
type LicenseCheckResponse = {
|
||||||
|
valid: boolean;
|
||||||
|
};
|
||||||
|
@Injectable()
|
||||||
|
export class DeploymentsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deploymentsRepository: DeploymentsRepository,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly redisService: RedisService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async checkLicense() {
|
||||||
|
if (this.configService.get("e2e")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let licenseKey = this.configService.get("api.licenseKey");
|
||||||
|
|
||||||
|
if (!licenseKey) {
|
||||||
|
/** We try to check on DB only if env is undefined */
|
||||||
|
const deployment = await this.deploymentsRepository.getDeployment();
|
||||||
|
licenseKey = deployment?.licenseKey ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!licenseKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const licenseKeyUrl = this.configService.get("api.licenseKeyUrl") + `?key=${licenseKey}`;
|
||||||
|
const cachedData = await this.redisService.redis.get(getLicenseCacheKey(licenseKey));
|
||||||
|
if (cachedData) {
|
||||||
|
return (JSON.parse(cachedData) as LicenseCheckResponse)?.valid;
|
||||||
|
}
|
||||||
|
const response = await fetch(licenseKeyUrl, { mode: "cors" });
|
||||||
|
const data = (await response.json()) as LicenseCheckResponse;
|
||||||
|
const cacheKey = getLicenseCacheKey(licenseKey);
|
||||||
|
this.redisService.redis.set(cacheKey, JSON.stringify(data), "EX", CACHING_TIME);
|
||||||
|
return data.valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
|||||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { UsersModule } from "./users/users.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [OAuthClientModule, BillingModule, PlatformEndpointsModule, TimezoneModule],
|
imports: [OAuthClientModule, BillingModule, PlatformEndpointsModule, TimezoneModule, UsersModule],
|
||||||
})
|
})
|
||||||
export class EndpointsModule implements NestModule {
|
export class EndpointsModule implements NestModule {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
|
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
|
||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { AuthModule } from "@/modules/auth/auth.module";
|
import { AuthModule } from "@/modules/auth/auth.module";
|
||||||
import { BillingModule } from "@/modules/billing/billing.module";
|
import { BillingModule } from "@/modules/billing/billing.module";
|
||||||
@@ -28,7 +28,7 @@ import { Global, Module } from "@nestjs/common";
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
EventTypesModule,
|
EventTypesModule_2024_04_15,
|
||||||
OrganizationsModule,
|
OrganizationsModule,
|
||||||
StripeModule,
|
StripeModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||||
@@ -14,7 +14,7 @@ export class OAuthClientUsersService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly userRepository: UsersRepository,
|
private readonly userRepository: UsersRepository,
|
||||||
private readonly tokensRepository: TokensRepository,
|
private readonly tokensRepository: TokensRepository,
|
||||||
private readonly eventTypesService: EventTypesService,
|
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||||
private readonly schedulesService: SchedulesService_2024_04_15
|
private readonly schedulesService: SchedulesService_2024_04_15
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TokenExpiredException } from "@/modules/auth/guards/access-token/token-expired.exception";
|
import { TokenExpiredException } from "@/modules/auth/guards/api-auth/token-expired.exception";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Organizations Team Endpoints", () => {
|
||||||
|
describe("User Authentication", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let org: Team;
|
||||||
|
|
||||||
|
const userEmail = "org-teams-controller-e2e@api.com";
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: "Test Organization",
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(organizationsRepositoryFixture).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
expect(org).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get all the teams of the org", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<Team[]> = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { GetOrg } from "@/modules/auth/decorators/get-org/get-org.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId/teams",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard)
|
||||||
|
@DocsTags("Organizations Teams")
|
||||||
|
export class OrganizationsTeamsController {
|
||||||
|
@Get()
|
||||||
|
async getAllTeams(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@GetOrg() organization: Team,
|
||||||
|
@GetOrg("name") orgName: string
|
||||||
|
): Promise<ApiResponse<Team[]>> {
|
||||||
|
console.log(orgId, organization, orgName);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/organizations-teams.controller";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -8,5 +9,6 @@ import { Module } from "@nestjs/common";
|
|||||||
imports: [PrismaModule, StripeModule],
|
imports: [PrismaModule, StripeModule],
|
||||||
providers: [OrganizationsRepository, OrganizationsService],
|
providers: [OrganizationsRepository, OrganizationsService],
|
||||||
exports: [OrganizationsService, OrganizationsRepository],
|
exports: [OrganizationsService, OrganizationsRepository],
|
||||||
|
controllers: [OrganizationsTeamsController],
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
@@ -16,6 +16,7 @@ export class OrganizationsRepository {
|
|||||||
return this.dbRead.prisma.team.findUnique({
|
return this.dbRead.prisma.team.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: organizationId,
|
id: organizationId,
|
||||||
|
isOrganization: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
|
||||||
import { SlotsService } from "@/modules/slots/services/slots.service";
|
import { SlotsService } from "@/modules/slots/services/slots.service";
|
||||||
import { Query, Body, Controller, Get, Delete, Post, Req, Res, UseGuards } from "@nestjs/common";
|
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||||
import { SlotsRepository } from "@/modules/slots/slots.repository";
|
import { SlotsRepository } from "@/modules/slots/slots.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
@@ -8,7 +8,7 @@ import { ReserveSlotInput } from "@calcom/platform-types";
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class SlotsService {
|
export class SlotsService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypeRepo: EventTypesRepository,
|
private readonly eventTypeRepo: EventTypesRepository_2024_04_15,
|
||||||
private readonly slotsRepo: SlotsRepository
|
private readonly slotsRepo: SlotsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 266 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user