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 | ||
|
|
87d2ec9e75 | ||
|
|
226686d4dd | ||
|
|
518ab82e3b | ||
|
|
532b78a9b3 | ||
|
|
1251091300 | ||
|
|
ce1459011a | ||
|
|
e1bdac077a | ||
|
|
d666f2926c | ||
|
|
745f0b0872 | ||
|
|
65f50e9f25 | ||
|
|
54162f8568 | ||
|
|
0f4b9b2a6e | ||
|
|
70b4e3a9c6 | ||
|
|
ab44d5b92c | ||
|
|
bed08c1e4f | ||
|
|
1e0f7f433f | ||
|
|
0cc7a9fc22 | ||
|
|
b03b878199 | ||
|
|
7a560cf09b | ||
|
|
58ad441223 | ||
|
|
d8de919acb | ||
|
|
d699b33dcb | ||
|
|
c071df4cd6 | ||
|
|
219b13d053 | ||
|
|
5e09281974 | ||
|
|
b66d36a98e | ||
|
|
68ce9524eb | ||
|
|
357a42b512 | ||
|
|
33d7da88bf | ||
|
|
cd888be61e | ||
|
|
06231ec299 | ||
|
|
e38705c3e8 | ||
|
|
5c95268d1a | ||
|
|
b310eb1a7e | ||
|
|
051510fa36 | ||
|
|
7d1226b823 | ||
|
|
7732a22c49 | ||
|
|
2a41cd0770 | ||
|
|
a574e5e60f | ||
|
|
0b6508379b | ||
|
|
f6930df87d | ||
|
|
7d4e8e2210 | ||
|
|
50eeb712e3 | ||
|
|
75edc4539c | ||
|
|
def87ea84a | ||
|
|
4153e5b2d5 | ||
|
|
8c2f6ae145 | ||
|
|
78ea4d2f68 | ||
|
|
e7c638c1e4 | ||
|
|
c28fab4fe2 | ||
|
|
e8a02554b6 | ||
|
|
4017c0e28f | ||
|
|
d72f7f8a42 | ||
|
|
3a4ebadbf3 | ||
|
|
765d601ff9 | ||
|
|
3a22fdb428 | ||
|
|
93580ac4bc | ||
|
|
b15ad6f233 | ||
|
|
56e59ab3e1 | ||
|
|
ee796933ee | ||
|
|
d2f76edfd6 | ||
|
|
822f0204a6 | ||
|
|
9770aff153 | ||
|
|
723d0ddec2 | ||
|
|
60b61eb625 | ||
|
|
5d2d99006b | ||
|
|
5d01eb2f38 | ||
|
|
ac3fb0a1cd | ||
|
|
1730ef7d5d | ||
|
|
9e371ded90 | ||
|
|
15098bdd1e | ||
|
|
625a7ec180 | ||
|
|
5602f7bd95 | ||
|
|
3e2caa9e1e | ||
|
|
b905de435d | ||
|
|
23bb06cc36 | ||
|
|
dd24100035 | ||
|
|
7434d91d48 | ||
|
|
3b1de34451 | ||
|
|
4cf89d273e | ||
|
|
e58ce4af71 | ||
|
|
15122dd5e9 | ||
|
|
793c0c60e3 | ||
|
|
344454e574 | ||
|
|
86342d103a | ||
|
|
d4f3726875 | ||
|
|
817a3decb7 | ||
|
|
5f082e11f8 | ||
|
|
5fb5081faa | ||
|
|
17f328fc5e | ||
|
|
d19034c2d5 | ||
|
|
4cdcc85620 | ||
|
|
3f51ae626c | ||
|
|
8499aebe88 | ||
|
|
07f613050e | ||
|
|
2c8c8368a0 | ||
|
|
74d3a84e27 | ||
|
|
33c1d49c52 | ||
|
|
889ff177c1 | ||
|
|
d8a6c375d0 | ||
|
|
655127d155 | ||
|
|
17c4a10e33 | ||
|
|
30f9f19a9f | ||
|
|
be7d45afe2 | ||
|
|
d2bec46d89 | ||
|
|
b90500778e | ||
|
|
bddc235c3c | ||
|
|
360b729d08 | ||
|
|
5d325495a9 | ||
|
|
dead2b843a | ||
|
|
1174cb0d8e | ||
|
|
58e6f18594 | ||
|
|
ffe6a46e66 |
No files matched your search
@@ -359,3 +359,6 @@ UNKEY_ROOT_KEY=
|
|||||||
# Used for Cal.ai Enterprise Voice AI Agents
|
# Used for Cal.ai Enterprise Voice AI Agents
|
||||||
# https://retellai.com
|
# https://retellai.com
|
||||||
RETELL_AI_KEY=
|
RETELL_AI_KEY=
|
||||||
|
|
||||||
|
# Used to disallow emails as being added as guests on bookings
|
||||||
|
BLACKLISTED_GUEST_EMAILS=
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
apps/api/v2/dist
|
||||||
|
packages/platform/**/dist/*
|
||||||
**/**/node_modules
|
**/**/node_modules
|
||||||
**/**/.next
|
**/**/.next
|
||||||
**/**/public
|
**/**/public
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-app-store:
|
e2e-app-store:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E App Store (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E App Store
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -70,9 +70,6 @@ jobs:
|
|||||||
- 1025:1025
|
- 1025:1025
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
## There aren't many tests for AppStore. So, just start with 2 shards. Increase if needed.
|
|
||||||
shard: [1, 2]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
@@ -93,10 +90,10 @@ jobs:
|
|||||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
run: yarn e2e:app-store
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: app-store-results
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -41,7 +41,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E Embed React (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E Embed React
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -62,9 +62,6 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
## There aren't many tests for embed-react. So, just start with 2 shards. Increase if needed.
|
|
||||||
shard: [1, 2]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
@@ -79,11 +76,11 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
yarn e2e:embed-react
|
||||||
yarn workspace @calcom/embed-react packaged:tests
|
yarn workspace @calcom/embed-react packaged:tests
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: embed-react-results
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -41,7 +41,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E Embed Core (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E Embed Core
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -70,9 +70,6 @@ jobs:
|
|||||||
- 1025:1025
|
- 1025:1025
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
## There aren't many tests for embed-core. So, just start with 2 shards. Increase if needed.
|
|
||||||
shard: [1, 2]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
@@ -86,10 +83,10 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
run: yarn e2e:embed
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: embed-core-results
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13
|
image: postgres:13
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shard: [1, 2, 3, 4, 5, 6, 7]
|
shard: [1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -37,12 +37,3 @@ jobs:
|
|||||||
```
|
```
|
||||||
${{ steps.lint_pr_title.outputs.error_message }}
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Delete a previous comment when the issue has been resolved
|
|
||||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
with:
|
|
||||||
header: pr-title-lint-error
|
|
||||||
message: |
|
|
||||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
|
|
||||||
|
|
||||||
+1
-1
@@ -99,7 +99,7 @@ To develop locally:
|
|||||||
|
|
||||||
- Duplicate `.env.example` to `.env`.
|
- Duplicate `.env.example` to `.env`.
|
||||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||||
|
|
||||||
6. Setup Node
|
6. Setup Node
|
||||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
export function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === "edge") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
};
|
};
|
||||||
@@ -55,7 +55,7 @@ export const schemaBookingReadPublic = Booking.extend({
|
|||||||
timeZone: true,
|
timeZone: true,
|
||||||
locale: true,
|
locale: true,
|
||||||
})
|
})
|
||||||
.optional(),
|
.nullish(),
|
||||||
payment: z
|
payment: z
|
||||||
.array(
|
.array(
|
||||||
_PaymentModel.pick({
|
_PaymentModel.pick({
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ export const withValidQueryIdTransformParseInt = withValidation({
|
|||||||
type: "Zod",
|
type: "Zod",
|
||||||
mode: "query",
|
mode: "query",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
|
||||||
|
recordingId: z.string(),
|
||||||
|
});
|
||||||
@@ -2,7 +2,12 @@ const { withAxiom } = require("next-axiom");
|
|||||||
const { withSentryConfig } = require("@sentry/nextjs");
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
|
|
||||||
const plugins = [withAxiom];
|
const plugins = [withAxiom];
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
instrumentationHook: true,
|
||||||
|
},
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@calcom/app-store",
|
"@calcom/app-store",
|
||||||
"@calcom/core",
|
"@calcom/core",
|
||||||
@@ -87,12 +92,12 @@ const nextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
nextConfig["sentry"] = {
|
plugins.push((nextConfig) =>
|
||||||
autoInstrumentServerFunctions: true,
|
withSentryConfig(nextConfig, {
|
||||||
hideSourceMaps: true,
|
autoInstrumentServerFunctions: true,
|
||||||
};
|
hideSourceMaps: true,
|
||||||
|
})
|
||||||
plugins.push(withSentryConfig);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@sentry/nextjs": "^7.73.0",
|
"@sentry/nextjs": "^8.8.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.5.4",
|
"next": "^13.5.4",
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
|
checkIfRoomNameMatchesInRecording,
|
||||||
|
} from "@calcom/core/videoClient";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { PartialReference } from "@calcom/types/EventManager";
|
||||||
|
|
||||||
|
import { getTranscriptFromRecordingId } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /bookings/{id}/transcripts/{recordingId}:
|
||||||
|
* get:
|
||||||
|
* summary: Find all Cal video transcripts of that recording
|
||||||
|
* operationId: getTranscriptsByRecordingId
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: ID of the booking for which transcripts need to be fetched.
|
||||||
|
* - in: path
|
||||||
|
* name: recordingId
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* required: true
|
||||||
|
* description: ID of the recording(daily.co recording id) for which transcripts need to be fetched.
|
||||||
|
* - in: query
|
||||||
|
* name: apiKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Your API key
|
||||||
|
* tags:
|
||||||
|
* - bookings
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* 401:
|
||||||
|
* description: Authorization information is missing or invalid.
|
||||||
|
* 404:
|
||||||
|
* description: Booking was not found
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getHandler(req: NextApiRequest) {
|
||||||
|
const { query } = req;
|
||||||
|
const { id, recordingId } = getTranscriptFromRecordingId.parse(query);
|
||||||
|
|
||||||
|
await checkIfRecordingBelongsToBooking(id, recordingId);
|
||||||
|
|
||||||
|
const transcriptsAccessLinks = await getTranscriptsAccessLinkFromRecordingId(recordingId);
|
||||||
|
|
||||||
|
return transcriptsAccessLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
|
||||||
|
const booking = await prisma.booking.findUnique({
|
||||||
|
where: { id: bookingId },
|
||||||
|
include: { references: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `No Booking found with booking id ${bookingId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const roomName =
|
||||||
|
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
if (!roomName)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `No Booking Reference with Daily Video found with booking id ${bookingId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canUserAccessRecordingId = await checkIfRoomNameMatchesInRecording(roomName, recordingId);
|
||||||
|
if (!canUserAccessRecordingId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: `This Recording Id ${recordingId} does not belong to booking ${bookingId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defaultResponder(getHandler);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
|
|
||||||
|
import authMiddleware from "../../_auth-middleware";
|
||||||
|
|
||||||
|
export default withMiddleware()(
|
||||||
|
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
await authMiddleware(req);
|
||||||
|
return defaultHandler({
|
||||||
|
GET: import("./_get"),
|
||||||
|
})(req, res);
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -2,6 +2,8 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
||||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||||
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
@@ -218,7 +220,16 @@ async function handler(req: NextApiRequest) {
|
|||||||
req.userId = requestedUserId || userId;
|
req.userId = requestedUserId || userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await handleNewBooking(req, getBookingDataSchemaForApi);
|
try {
|
||||||
|
return await handleNewBooking(req, getBookingDataSchemaForApi);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const knownError = error as Error;
|
||||||
|
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: knownError.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(handler);
|
export default defaultResponder(handler);
|
||||||
@@ -7,7 +7,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user";
|
|||||||
|
|
||||||
async function handler({ userId }: NextApiRequest) {
|
async function handler({ userId }: NextApiRequest) {
|
||||||
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
||||||
return { user: schemaUserReadPublic.parse(data) };
|
return {
|
||||||
|
user: schemaUserReadPublic.parse({
|
||||||
|
...data,
|
||||||
|
avatar: data.avatarUrl,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(handler);
|
export default defaultResponder(handler);
|
||||||
@@ -45,7 +45,10 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
if (!isSystemWideAdmin && query.userId !== req.userId)
|
if (!isSystemWideAdmin && query.userId !== req.userId)
|
||||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
const data = await prisma.user.findUnique({ where: { id: query.userId } });
|
const data = await prisma.user.findUnique({ where: { id: query.userId } });
|
||||||
const user = schemaUserReadPublic.parse(data);
|
const user = schemaUserReadPublic.parse({
|
||||||
|
...data,
|
||||||
|
avatar: data?.avatarUrl,
|
||||||
|
});
|
||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
|
import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
|
||||||
import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validations/user";
|
import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validations/user";
|
||||||
@@ -101,7 +103,8 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
if (!isSystemWideAdmin && query.userId !== req.userId)
|
if (!isSystemWideAdmin && query.userId !== req.userId)
|
||||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|
||||||
const body = await schemaUserEditBodyParams.parseAsync(req.body);
|
const { avatar, ...body }: { avatar?: string | undefined } & Prisma.UserUpdateInput =
|
||||||
|
await schemaUserEditBodyParams.parseAsync(req.body);
|
||||||
// disable role or branding changes unless admin.
|
// disable role or branding changes unless admin.
|
||||||
if (!isSystemWideAdmin) {
|
if (!isSystemWideAdmin) {
|
||||||
if (body.role) body.role = undefined;
|
if (body.role) body.role = undefined;
|
||||||
@@ -119,6 +122,14 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
message: "Bad request: Invalid default schedule id",
|
message: "Bad request: Invalid default schedule id",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (avatar) {
|
||||||
|
body.avatarUrl = await uploadAvatar({
|
||||||
|
userId: query.userId,
|
||||||
|
avatar: await (await import("@calcom/lib/server/resizeBase64Image")).resizeBase64Image(avatar),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const data = await prisma.user.update({
|
const data = await prisma.user.update({
|
||||||
where: { id: query.userId },
|
where: { id: query.userId },
|
||||||
data: body,
|
data: body,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { init as SentryInit } from "@sentry/nextjs";
|
|
||||||
|
|
||||||
SentryInit({
|
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { init as SentryInit } from "@sentry/nextjs";
|
|
||||||
|
|
||||||
SentryInit({
|
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import prismaMock from "../../../../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
|
checkIfRoomNameMatchesInRecording,
|
||||||
|
} from "@calcom/core/videoClient";
|
||||||
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
|
import authMiddleware from "../../../../../../pages/api/bookings/[id]/_auth-middleware";
|
||||||
|
import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[recordingId]/_get";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
vi.mock("@calcom/core/videoClient", () => {
|
||||||
|
return {
|
||||||
|
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
||||||
|
checkIfRoomNameMatchesInRecording: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => {
|
||||||
|
return {
|
||||||
|
getAccessibleUsers: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockGetTranscripts = () => {
|
||||||
|
const downloadLinks = [{ format: "json", link: "https://URL1" }];
|
||||||
|
|
||||||
|
vi.mocked(getTranscriptsAccessLinkFromRecordingId).mockResolvedValue(downloadLinks);
|
||||||
|
vi.mocked(checkIfRoomNameMatchesInRecording).mockResolvedValue(true);
|
||||||
|
|
||||||
|
return downloadLinks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordingId = "abc-xyz";
|
||||||
|
|
||||||
|
describe("GET /api/bookings/[id]/transcripts/[recordingId]", () => {
|
||||||
|
test("Returns transcripts if user is system-wide admin", async () => {
|
||||||
|
const adminUserId = 1;
|
||||||
|
const userId = 2;
|
||||||
|
|
||||||
|
const bookingId = 1111;
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(
|
||||||
|
buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId,
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: "daily_video",
|
||||||
|
uid: "17OHkCH53pBa03FhxMbw",
|
||||||
|
meetingId: "17OHkCH53pBa03FhxMbw",
|
||||||
|
meetingPassword: "password",
|
||||||
|
meetingUrl: "https://URL",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockedTranscripts = mockGetTranscripts();
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: bookingId,
|
||||||
|
recordingId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isSystemWideAdmin = true;
|
||||||
|
req.userId = adminUserId;
|
||||||
|
|
||||||
|
await authMiddleware(req);
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res._getData())).toEqual(mockedTranscripts);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Allows GET transcripts when user is org-wide admin", async () => {
|
||||||
|
const adminUserId = 1;
|
||||||
|
const memberUserId = 10;
|
||||||
|
const bookingId = 3333;
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(
|
||||||
|
buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: memberUserId,
|
||||||
|
references: [
|
||||||
|
{ id: 1, type: "daily_video", uid: "17OHkCH53pBa03FhxMbw", meetingId: "17OHkCH53pBa03FhxMbw" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: bookingId,
|
||||||
|
recordingId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = adminUserId;
|
||||||
|
req.isOrganizationOwnerOrAdmin = true;
|
||||||
|
mockGetTranscripts();
|
||||||
|
|
||||||
|
vi.mocked(getAccessibleUsers).mockResolvedValue([memberUserId]);
|
||||||
|
|
||||||
|
await authMiddleware(req);
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
@@ -2,5 +2,6 @@ const rootConfig = require("../../../packages/config/prettier-preset");
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...rootConfig,
|
...rootConfig,
|
||||||
|
importOrder: ["^./instrument", ...rootConfig.importOrder],
|
||||||
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
||||||
};
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"build": "yarn dev:build && nest build",
|
"build": "yarn dev:build && nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch & yarn workspace @calcom/platform-libraries build:watch",
|
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
|
||||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
|
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
|
||||||
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
||||||
@@ -25,11 +25,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-libraries": "*",
|
"@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.4": "npm:@calcom/platform-libraries@0.0.4",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@golevelup/ts-jest": "^0.4.0",
|
"@golevelup/ts-jest": "^0.4.0",
|
||||||
|
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
@@ -38,8 +41,7 @@
|
|||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
"@nestjs/throttler": "^5.1.2",
|
"@nestjs/throttler": "^5.1.2",
|
||||||
"@sentry/node": "^7.86.0",
|
"@sentry/node": "^8.8.0",
|
||||||
"@sentry/tracing": "^7.86.0",
|
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
"querystring": "^0.2.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"stripe": "^15.3.0",
|
"stripe": "^15.3.0",
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ import { AppController } from "./app.controller";
|
|||||||
EndpointsModule,
|
EndpointsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
JwtModule,
|
JwtModule,
|
||||||
//register prefix for all routes in EndpointsModule
|
|
||||||
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
|
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
+34
-15
@@ -1,26 +1,41 @@
|
|||||||
import { getEnv } from "@/env";
|
import "./instrument";
|
||||||
|
|
||||||
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 { SentryFilter } from "@/filters/sentry-exception.filter";
|
|
||||||
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
|
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
|
||||||
import type { ValidationError } from "@nestjs/common";
|
import type { ValidationError } from "@nestjs/common";
|
||||||
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
|
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
|
||||||
import { HttpAdapterHost } from "@nestjs/core";
|
import { BaseExceptionFilter, HttpAdapterHost } from "@nestjs/core";
|
||||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import * as cookieParser from "cookie-parser";
|
import * as cookieParser from "cookie-parser";
|
||||||
|
import { Request } from "express";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
|
|
||||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
import {
|
||||||
|
API_VERSIONS,
|
||||||
|
VERSION_2024_04_15,
|
||||||
|
API_VERSIONS_ENUM,
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
X_CAL_SECRET_KEY,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
|
||||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||||
|
|
||||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
app.enableVersioning({
|
app.enableVersioning({
|
||||||
type: VersioningType.URI,
|
type: VersioningType.CUSTOM,
|
||||||
prefix: "v",
|
extractor: (request: unknown) => {
|
||||||
defaultVersion: "1",
|
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
|
||||||
|
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
|
||||||
|
return headerVersion;
|
||||||
|
}
|
||||||
|
return VERSION_2024_04_15;
|
||||||
|
},
|
||||||
|
defaultVersion: VERSION_2024_04_15,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
@@ -28,7 +43,15 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: "*",
|
origin: "*",
|
||||||
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
||||||
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
|
allowedHeaders: [
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
X_CAL_SECRET_KEY,
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
"Accept",
|
||||||
|
"Authorization",
|
||||||
|
"Content-Type",
|
||||||
|
"Origin",
|
||||||
|
],
|
||||||
maxAge: 86_400,
|
maxAge: 86_400,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,15 +69,11 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (process.env.SENTRY_DSN) {
|
|
||||||
Sentry.init({
|
|
||||||
dsn: getEnv("SENTRY_DSN"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exception filters, new filters go at the bottom, keep the order
|
// Exception filters, new filters go at the bottom, keep the order
|
||||||
const { httpAdapter } = app.get(HttpAdapterHost);
|
const { httpAdapter } = app.get(HttpAdapterHost);
|
||||||
app.useGlobalFilters(new SentryFilter(httpAdapter));
|
if (process.env.SENTRY_DSN) {
|
||||||
|
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
|
||||||
|
}
|
||||||
app.useGlobalFilters(new PrismaExceptionFilter());
|
app.useGlobalFilters(new PrismaExceptionFilter());
|
||||||
app.useGlobalFilters(new ZodExceptionFilter());
|
app.useGlobalFilters(new ZodExceptionFilter());
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -2,11 +2,10 @@ import { bootstrap } from "@/app";
|
|||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
@@ -17,10 +16,10 @@ 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";
|
import { handleNewBooking } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Bookings Endpoints", () => {
|
describe("Bookings Endpoints", () => {
|
||||||
@@ -29,7 +28,7 @@ describe("Bookings Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
let schedulesService: SchedulesService;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
const userEmail = "bookings-controller-e2e@api.com";
|
const userEmail = "bookings-controller-e2e@api.com";
|
||||||
@@ -40,10 +39,10 @@ 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, AvailabilitiesModule, UsersModule, SchedulesModule],
|
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -55,13 +54,13 @@ describe("Bookings Endpoints", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService>(SchedulesService);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userSchedule: CreateScheduleInput = {
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
name: "working time",
|
name: "working time",
|
||||||
timeZone: "Europe/Rome",
|
timeZone: "Europe/Rome",
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
|||||||
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
||||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||||
|
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";
|
||||||
@@ -37,14 +38,14 @@ import {
|
|||||||
getBookingInfo,
|
getBookingInfo,
|
||||||
handleCancelBooking,
|
handleCancelBooking,
|
||||||
getBookingForReschedule,
|
getBookingForReschedule,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import {
|
import {
|
||||||
handleNewBooking,
|
handleNewBooking,
|
||||||
BookingResponse,
|
BookingResponse,
|
||||||
HttpError,
|
HttpError,
|
||||||
handleNewRecurringBooking,
|
handleNewRecurringBooking,
|
||||||
handleInstantMeeting,
|
handleInstantMeeting,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
@@ -72,8 +73,8 @@ const DEFAULT_PLATFORM_PARAMS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/bookings",
|
path: "/v2/bookings",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Bookings")
|
@DocsTags("Bookings")
|
||||||
@@ -88,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 })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||||
|
|
||||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
import type { AppsStatus } from "@calcom/platform-libraries-0.0.2";
|
||||||
|
|
||||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
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, Credential } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.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 {
|
||||||
|
GOOGLE_CALENDAR,
|
||||||
|
OFFICE_365_CALENDAR,
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
GOOGLE_CALENDAR_ID,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||||
|
|
||||||
|
class CalendarsServiceMock {
|
||||||
|
async getCalendars() {
|
||||||
|
return {
|
||||||
|
connectedCalendars: [
|
||||||
|
{
|
||||||
|
integration: {
|
||||||
|
type: "google_calendar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Platform Calendars Endpoints", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let organization: Team;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||||
|
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
|
||||||
|
let user: User;
|
||||||
|
let office365Credentials: Credential;
|
||||||
|
let googleCalendarCredentials: Credential;
|
||||||
|
let accessTokenSecret: string;
|
||||||
|
let refreshTokenSecret: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.overrideProvider(CalendarsService)
|
||||||
|
.useClass(CalendarsServiceMock)
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||||
|
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
|
||||||
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
|
||||||
|
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
|
||||||
|
accessTokenSecret = tokens.accessToken;
|
||||||
|
refreshTokenSecret = tokens.refreshToken;
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: [CLIENT_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(accessTokenSecret).toBeDefined();
|
||||||
|
expect(refreshTokenSecret).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should respond 401 with invalid access token`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
|
||||||
|
.set("Authorization", `Bearer invalid_access_token`)
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should redirect to auth-url for office 365 calendar oauth with valid access token `, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
const data = response.body.data;
|
||||||
|
expect(data.authUrl).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar oauth with valid access token `, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
const data = response.body.data;
|
||||||
|
expect(data.authUrl).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/random-calendar/connect: it should respond 400 with a message saying the calendar type is invalid`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/random-calendar/connect`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/save: without access token`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(
|
||||||
|
`/v2/calendars/${OFFICE_365_CALENDAR}/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=User.Read%20Calendars.Read%20Calendars.ReadWrite%20offline_access`
|
||||||
|
)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/save: without origin`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(
|
||||||
|
`/v2/calendars/${OFFICE_365_CALENDAR}/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=User.Read%20Calendars.Read%20Calendars.ReadWrite%20offline_access`
|
||||||
|
)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check without access token`, async () => {
|
||||||
|
await request(app.getHttpServer()).get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`).expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check with no credentials`, async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/check with access token, origin and office365 credentials`, async () => {
|
||||||
|
office365Credentials = await credentialsRepositoryFixture.create(
|
||||||
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
|
{},
|
||||||
|
user.id,
|
||||||
|
OFFICE_365_CALENDAR_ID
|
||||||
|
);
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${OFFICE_365_CALENDAR}/check`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/check with access token, origin and google calendar credentials`, async () => {
|
||||||
|
googleCalendarCredentials = await credentialsRepositoryFixture.create(
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
{},
|
||||||
|
user.id,
|
||||||
|
GOOGLE_CALENDAR_ID
|
||||||
|
);
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars/${GOOGLE_CALENDAR}/check`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
await credentialsRepositoryFixture.delete(office365Credentials.id);
|
||||||
|
await credentialsRepositoryFixture.delete(googleCalendarCredentials.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export interface CalendarApp {
|
||||||
|
save(state: string, code: string, origin: string): Promise<{ url: string }>;
|
||||||
|
check(userId: number): Promise<ApiResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OAuthCalendarApp extends CalendarApp {
|
||||||
|
connect(authorization: string, req: Request): Promise<ApiResponse<{ authUrl: string }>>;
|
||||||
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||||
|
import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, UsersModule],
|
imports: [PrismaModule, UsersModule, TokensModule],
|
||||||
providers: [CredentialsRepository, CalendarsService],
|
providers: [
|
||||||
|
CredentialsRepository,
|
||||||
|
CalendarsService,
|
||||||
|
OutlookService,
|
||||||
|
GoogleCalendarService,
|
||||||
|
SelectedCalendarsRepository,
|
||||||
|
AppsRepository,
|
||||||
|
],
|
||||||
controllers: [CalendarsController],
|
controllers: [CalendarsController],
|
||||||
exports: [CalendarsService],
|
exports: [CalendarsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,24 +1,48 @@
|
|||||||
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
|
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
|
||||||
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
|
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
|
||||||
|
import { OutlookService } from "@/ee/calendars/services/outlook.service";
|
||||||
|
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 { 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 { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
UseGuards,
|
||||||
|
Query,
|
||||||
|
HttpStatus,
|
||||||
|
HttpCode,
|
||||||
|
Req,
|
||||||
|
Param,
|
||||||
|
Headers,
|
||||||
|
Redirect,
|
||||||
|
BadRequestException,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { APPS_READ } from "@calcom/platform-constants";
|
||||||
import { CalendarBusyTimesInput } from "@calcom/platform-types";
|
import { SUCCESS_STATUS, CALENDARS, GOOGLE_CALENDAR, OFFICE_365_CALENDAR } from "@calcom/platform-constants";
|
||||||
|
import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/calendars",
|
path: "/v2/calendars",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard)
|
|
||||||
@DocsTags("Calendars")
|
@DocsTags("Calendars")
|
||||||
export class CalendarsController {
|
export class CalendarsController {
|
||||||
constructor(private readonly calendarsService: CalendarsService) {}
|
constructor(
|
||||||
|
private readonly calendarsService: CalendarsService,
|
||||||
|
private readonly outlookService: OutlookService,
|
||||||
|
private readonly googleCalendarService: GoogleCalendarService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
@Get("/busy-times")
|
@Get("/busy-times")
|
||||||
async getBusyTimes(
|
async getBusyTimes(
|
||||||
@Query() queryParams: CalendarBusyTimesInput,
|
@Query() queryParams: CalendarBusyTimesInput,
|
||||||
@@ -47,6 +71,7 @@ export class CalendarsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
|
@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);
|
||||||
|
|
||||||
@@ -55,4 +80,74 @@ export class CalendarsController {
|
|||||||
data: calendars,
|
data: calendars,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@Get("/:calendar/connect")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async redirect(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Headers("Authorization") authorization: string,
|
||||||
|
@Param("calendar") calendar: string,
|
||||||
|
@Query("redir") redir?: string | null
|
||||||
|
): Promise<ApiResponse<{ authUrl: string }>> {
|
||||||
|
switch (calendar) {
|
||||||
|
case OFFICE_365_CALENDAR:
|
||||||
|
return await this.outlookService.connect(authorization, req, redir ?? "");
|
||||||
|
case GOOGLE_CALENDAR:
|
||||||
|
return await this.googleCalendarService.connect(authorization, req, redir ?? "");
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
CALENDARS.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:calendar/save")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Redirect(undefined, 301)
|
||||||
|
async save(
|
||||||
|
@Query("state") state: string,
|
||||||
|
@Query("code") code: string,
|
||||||
|
@Param("calendar") calendar: string
|
||||||
|
): Promise<{ url: string }> {
|
||||||
|
// state params contains our user access token
|
||||||
|
const stateParams = new URLSearchParams(state);
|
||||||
|
const { accessToken, origin, redir } = z
|
||||||
|
.object({ accessToken: z.string(), origin: z.string(), redir: z.string().nullish().optional() })
|
||||||
|
.parse({
|
||||||
|
accessToken: stateParams.get("accessToken"),
|
||||||
|
origin: stateParams.get("origin"),
|
||||||
|
redir: stateParams.get("redir"),
|
||||||
|
});
|
||||||
|
switch (calendar) {
|
||||||
|
case OFFICE_365_CALENDAR:
|
||||||
|
return await this.outlookService.save(code, accessToken, origin, redir ?? "");
|
||||||
|
case GOOGLE_CALENDAR:
|
||||||
|
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "");
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
CALENDARS.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:calendar/check")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
|
@Permissions([APPS_READ])
|
||||||
|
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
|
||||||
|
switch (calendar) {
|
||||||
|
case OFFICE_365_CALENDAR:
|
||||||
|
return await this.outlookService.check(userId);
|
||||||
|
case GOOGLE_CALENDAR:
|
||||||
|
return await this.googleCalendarService.check(userId);
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
CALENDARS.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import {
|
import {
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
CredentialsWithUserEmail,
|
CredentialsWithUserEmail,
|
||||||
@@ -11,21 +12,27 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
import { getConnectedDestinationCalendars } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { getBusyCalendarTimes } from "@calcom/platform-libraries";
|
import { getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { Calendar } from "@calcom/platform-types";
|
import { Calendar } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CalendarsService {
|
export class CalendarsService {
|
||||||
|
private oAuthCalendarResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly credentialsRepository: CredentialsRepository,
|
private readonly credentialsRepository: CredentialsRepository,
|
||||||
|
private readonly appsRepository: AppsRepository,
|
||||||
private readonly dbRead: PrismaReadService,
|
private readonly dbRead: PrismaReadService,
|
||||||
private readonly dbWrite: PrismaWriteService
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private readonly config: ConfigService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCalendars(userId: number) {
|
async getCalendars(userId: number) {
|
||||||
@@ -110,4 +117,24 @@ export class CalendarsService {
|
|||||||
});
|
});
|
||||||
return composedSelectedCalendars;
|
return composedSelectedCalendars;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAppKeys(appName: string) {
|
||||||
|
const app = await this.appsRepository.getAppBySlug(appName);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client_id, client_secret } = this.oAuthCalendarResponseSchema.parse(app.keys);
|
||||||
|
|
||||||
|
if (!client_id) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client_secret) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { client_id, client_secret };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import { Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { google } from "googleapis";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
const CALENDAR_SCOPES = [
|
||||||
|
"https://www.googleapis.com/auth/calendar.readonly",
|
||||||
|
"https://www.googleapis.com/auth/calendar.events",
|
||||||
|
];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GoogleCalendarService implements OAuthCalendarApp {
|
||||||
|
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||||
|
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||||
|
private logger = new Logger("GcalService");
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly appsRepository: AppsRepository,
|
||||||
|
private readonly credentialRepository: CredentialsRepository,
|
||||||
|
private readonly calendarsService: CalendarsService,
|
||||||
|
private readonly tokensRepository: TokensRepository,
|
||||||
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async connect(
|
||||||
|
authorization: string,
|
||||||
|
req: Request,
|
||||||
|
redir?: string
|
||||||
|
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
|
||||||
|
const accessToken = authorization.replace("Bearer ", "");
|
||||||
|
const origin = req.get("origin") ?? req.get("host");
|
||||||
|
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||||
|
|
||||||
|
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
|
||||||
|
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
|
||||||
|
}
|
||||||
|
|
||||||
|
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
return await this.checkIfCalendarConnected(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
|
||||||
|
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
||||||
|
|
||||||
|
const authUrl = oAuth2Client.generateAuthUrl({
|
||||||
|
access_type: "offline",
|
||||||
|
scope: CALENDAR_SCOPES,
|
||||||
|
prompt: "consent",
|
||||||
|
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return authUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOAuthClient(redirectUri: string) {
|
||||||
|
this.logger.log("Getting Google Calendar OAuth Client");
|
||||||
|
const app = await this.appsRepository.getAppBySlug("google-calendar");
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||||
|
|
||||||
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||||
|
return oAuth2Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
||||||
|
|
||||||
|
if (!gcalCredentials) {
|
||||||
|
throw new BadRequestException("Credentials for google_calendar not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gcalCredentials.invalid) {
|
||||||
|
throw new BadRequestException("Invalid google oauth credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
|
||||||
|
const googleCalendar = connectedCalendars.find(
|
||||||
|
(cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
if (!googleCalendar) {
|
||||||
|
throw new UnauthorizedException("Google Calendar not connected.");
|
||||||
|
}
|
||||||
|
if (googleCalendar.error?.message) {
|
||||||
|
throw new UnauthorizedException(googleCalendar.error?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: SUCCESS_STATUS };
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveCalendarCredentialsAndRedirect(
|
||||||
|
code: string,
|
||||||
|
accessToken: string,
|
||||||
|
origin: string,
|
||||||
|
redir?: string
|
||||||
|
) {
|
||||||
|
// User chose not to authorize your app or didn't authorize your app
|
||||||
|
// redirect directly without oauth code
|
||||||
|
if (!code) {
|
||||||
|
return { url: redir || origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedCode = z.string().parse(code);
|
||||||
|
|
||||||
|
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||||
|
|
||||||
|
if (!ownerId) {
|
||||||
|
throw new UnauthorizedException("Invalid Access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
||||||
|
const token = await oAuth2Client.getToken(parsedCode);
|
||||||
|
// Google oAuth Credentials are stored in token.tokens
|
||||||
|
const key = token.tokens;
|
||||||
|
const credential = await this.credentialRepository.createAppCredential(
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
key as Prisma.InputJsonValue,
|
||||||
|
ownerId
|
||||||
|
);
|
||||||
|
|
||||||
|
oAuth2Client.setCredentials(key);
|
||||||
|
|
||||||
|
const calendar = google.calendar({
|
||||||
|
version: "v3",
|
||||||
|
auth: oAuth2Client,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
|
||||||
|
|
||||||
|
const primaryCal = cals.data.items?.find((cal) => cal.primary);
|
||||||
|
|
||||||
|
if (primaryCal?.id) {
|
||||||
|
await this.selectedCalendarsRepository.createSelectedCalendar(
|
||||||
|
primaryCal.id,
|
||||||
|
credential.id,
|
||||||
|
ownerId,
|
||||||
|
GOOGLE_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url: redir || origin };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import type { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
|
||||||
|
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { stringify } from "querystring";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
OFFICE_365_CALENDAR,
|
||||||
|
OFFICE_365_CALENDAR_ID,
|
||||||
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OutlookService implements OAuthCalendarApp {
|
||||||
|
private redirectUri = `${this.config.get("api.url")}/calendars/${OFFICE_365_CALENDAR}/save`;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly calendarsService: CalendarsService,
|
||||||
|
private readonly credentialRepository: CredentialsRepository,
|
||||||
|
private readonly tokensRepository: TokensRepository,
|
||||||
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async connect(
|
||||||
|
authorization: string,
|
||||||
|
req: Request,
|
||||||
|
redir?: string
|
||||||
|
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
|
||||||
|
const accessToken = authorization.replace("Bearer ", "");
|
||||||
|
const origin = req.get("origin") ?? req.get("host");
|
||||||
|
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||||
|
|
||||||
|
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
|
||||||
|
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
|
||||||
|
}
|
||||||
|
|
||||||
|
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
return await this.checkIfCalendarConnected(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
|
||||||
|
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
||||||
|
|
||||||
|
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
||||||
|
const params = {
|
||||||
|
response_type: "code",
|
||||||
|
scope: scopes.join(" "),
|
||||||
|
client_id,
|
||||||
|
prompt: "select_account",
|
||||||
|
redirect_uri: this.redirectUri,
|
||||||
|
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = stringify(params);
|
||||||
|
|
||||||
|
const url = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${query}`;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||||
|
const office365CalendarCredentials = await this.credentialRepository.getByTypeAndUserId(
|
||||||
|
"office365_calendar",
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!office365CalendarCredentials) {
|
||||||
|
throw new BadRequestException("Credentials for office_365_calendar not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (office365CalendarCredentials.invalid) {
|
||||||
|
throw new BadRequestException("Invalid office 365 calendar credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
|
||||||
|
const office365Calendar = connectedCalendars.find(
|
||||||
|
(cal: { integration: { type: string } }) => cal.integration.type === OFFICE_365_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
if (!office365Calendar) {
|
||||||
|
throw new UnauthorizedException("Office 365 calendar not connected.");
|
||||||
|
}
|
||||||
|
if (office365Calendar.error?.message) {
|
||||||
|
throw new UnauthorizedException(office365Calendar.error?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOAuthCredentials(code: string) {
|
||||||
|
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
|
||||||
|
const { client_id, client_secret } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
||||||
|
|
||||||
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
|
Object.keys(payload)
|
||||||
|
.map((key) => `${key}=${encodeURIComponent(payload[key])}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
const body = toUrlEncoded({
|
||||||
|
client_id,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
scope: scopes.join(" "),
|
||||||
|
redirect_uri: this.redirectUri,
|
||||||
|
client_secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await response.json();
|
||||||
|
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefaultCalendar(accessToken: string): Promise<OfficeCalendar> {
|
||||||
|
const response = await fetch("https://graph.microsoft.com/v1.0/me/calendar", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseBody = await response.json();
|
||||||
|
|
||||||
|
return responseBody as OfficeCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveCalendarCredentialsAndRedirect(
|
||||||
|
code: string,
|
||||||
|
accessToken: string,
|
||||||
|
origin: string,
|
||||||
|
redir?: string
|
||||||
|
) {
|
||||||
|
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
|
||||||
|
if (!code) {
|
||||||
|
return { url: redir || origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedCode = z.string().parse(code);
|
||||||
|
|
||||||
|
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||||
|
|
||||||
|
if (!ownerId) {
|
||||||
|
throw new UnauthorizedException("Invalid Access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const office365OAuthCredentials = await this.getOAuthCredentials(parsedCode);
|
||||||
|
|
||||||
|
const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token);
|
||||||
|
|
||||||
|
if (defaultCalendar?.id) {
|
||||||
|
const credential = await this.credentialRepository.createAppCredential(
|
||||||
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
|
office365OAuthCredentials,
|
||||||
|
ownerId
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.selectedCalendarsRepository.createSelectedCalendar(
|
||||||
|
defaultCalendar.id,
|
||||||
|
credential.id,
|
||||||
|
ownerId,
|
||||||
|
OFFICE_365_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: redir || origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
+69
-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,15 +22,20 @@ 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,
|
||||||
eventTypeBookingFields,
|
eventTypeBookingFields,
|
||||||
eventTypeLocations,
|
eventTypeLocations,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Event types Endpoints", () => {
|
describe("Event types Endpoints", () => {
|
||||||
@@ -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) => {
|
||||||
@@ -204,10 +211,20 @@ describe("Event types Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return 400 if param event type id is null", async () => {
|
||||||
|
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
||||||
|
|
||||||
|
const body: UpdateEventTypeInput_2024_04_15 = {
|
||||||
|
locations,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer()).patch(`/api/v2/event-types/null`).send(body).expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -256,7 +273,7 @@ describe("Event types Endpoints", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const body: UpdateEventTypeInput = {
|
const body: UpdateEventTypeInput_2024_04_15 = {
|
||||||
bookingFields,
|
bookingFields,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,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);
|
||||||
|
|
||||||
@@ -295,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);
|
||||||
|
|
||||||
@@ -313,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);
|
||||||
|
|
||||||
@@ -330,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);
|
||||||
|
|
||||||
@@ -349,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);
|
||||||
|
|
||||||
@@ -365,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 () => {
|
||||||
+37
-29
@@ -1,17 +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 { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
import { EventTypeIdParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { GetPublicEventTypeQueryParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input";
|
||||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||||
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
import { CreateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output";
|
||||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
import { DeleteEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output";
|
||||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
|
||||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output";
|
||||||
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output";
|
||||||
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
import {
|
||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
GetEventTypesData,
|
||||||
|
GetEventTypesOutput,
|
||||||
|
} 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";
|
||||||
@@ -29,31 +34,32 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
ParseIntPipe,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { getPublicEvent } from "@calcom/platform-libraries";
|
import { getPublicEvent } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
import { getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "event-types",
|
path: "/v2/event-types",
|
||||||
version: "2",
|
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);
|
||||||
@@ -66,9 +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("eventTypeId") eventTypeId: string,
|
@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));
|
||||||
@@ -85,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,
|
||||||
@@ -104,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(
|
||||||
@@ -141,11 +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("eventTypeId") eventTypeId: number,
|
@Param() params: EventTypeIdParams_2024_04_15,
|
||||||
@Body() body: UpdateEventTypeInput,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@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);
|
||||||
@@ -158,9 +165,10 @@ export class EventTypesController {
|
|||||||
|
|
||||||
@Delete("/:eventTypeId")
|
@Delete("/:eventTypeId")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async deleteEventType(
|
async deleteEventType(
|
||||||
@Param("eventTypeId") eventTypeId: number,
|
@Param() params: EventTypeIdParams_2024_04_15,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@GetUser("id") userId: number
|
@GetUser("id") userId: number
|
||||||
): Promise<DeleteEventTypeOutput> {
|
): Promise<DeleteEventTypeOutput> {
|
||||||
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||||
@@ -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 {}
|
||||||
+4
-4
@@ -1,19 +1,19 @@
|
|||||||
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";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
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.
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsNumberString } from "class-validator";
|
||||||
|
|
||||||
|
export class EventTypeIdParams_2024_04_15 {
|
||||||
|
@IsNumberString()
|
||||||
|
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";
|
||||||
+15
-12
@@ -1,29 +1,32 @@
|
|||||||
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";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.2";
|
||||||
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 || {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,11 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
|||||||
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
|
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
|
||||||
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
|
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
|
||||||
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
|
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
|
||||||
|
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";
|
||||||
@@ -40,8 +41,8 @@ const CALENDAR_SCOPES = [
|
|||||||
|
|
||||||
// Controller for the GCalConnect Atom
|
// Controller for the GCalConnect Atom
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/gcal",
|
path: "/v2/gcal",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsTags("Google Calendar")
|
@DocsTags("Google Calendar")
|
||||||
export class GcalController {
|
export class GcalController {
|
||||||
@@ -60,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
|
||||||
@@ -81,61 +82,15 @@ export class GcalController {
|
|||||||
@Redirect(undefined, 301)
|
@Redirect(undefined, 301)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
|
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
|
||||||
const stateParams = new URLSearchParams(state);
|
const url = new URL(this.config.get("api.url") + "/calendars/google/save");
|
||||||
const { accessToken, origin } = z
|
url.searchParams.append("code", code);
|
||||||
.object({ accessToken: z.string(), origin: z.string() })
|
url.searchParams.append("state", state);
|
||||||
.parse({ accessToken: stateParams.get("accessToken"), origin: stateParams.get("origin") });
|
return { url: url.href };
|
||||||
|
|
||||||
// User chose not to authorize your app or didn't authorize your app
|
|
||||||
// redirect directly without oauth code
|
|
||||||
if (!code) {
|
|
||||||
return { url: origin };
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedCode = z.string().parse(code);
|
|
||||||
|
|
||||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
|
||||||
|
|
||||||
if (!ownerId) {
|
|
||||||
throw new UnauthorizedException("Invalid Access token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
|
|
||||||
const token = await oAuth2Client.getToken(parsedCode);
|
|
||||||
// Google oAuth Credentials are stored in token.tokens
|
|
||||||
const key = token.tokens;
|
|
||||||
const credential = await this.credentialRepository.createAppCredential(
|
|
||||||
GOOGLE_CALENDAR_TYPE,
|
|
||||||
key as Prisma.InputJsonValue,
|
|
||||||
ownerId
|
|
||||||
);
|
|
||||||
|
|
||||||
oAuth2Client.setCredentials(key);
|
|
||||||
|
|
||||||
const calendar = google.calendar({
|
|
||||||
version: "v3",
|
|
||||||
auth: oAuth2Client,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
|
|
||||||
|
|
||||||
const primaryCal = cals.data.items?.find((cal) => cal.primary);
|
|
||||||
|
|
||||||
if (primaryCal?.id) {
|
|
||||||
await this.selectedCalendarsRepository.createSelectedCalendar(
|
|
||||||
primaryCal.id,
|
|
||||||
credential.id,
|
|
||||||
ownerId,
|
|
||||||
GOOGLE_CALENDAR_TYPE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { url: origin };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
@@ -14,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";
|
||||||
@@ -31,17 +30,10 @@ 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: [
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||||
AppModule,
|
|
||||||
PrismaModule,
|
|
||||||
AvailabilitiesModule,
|
|
||||||
UsersModule,
|
|
||||||
TokensModule,
|
|
||||||
SchedulesModule,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -118,13 +110,13 @@ describe("Me Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid time format", async () => {
|
it("should not update user associated with access token given invalid time format", async () => {
|
||||||
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
|
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 as any };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid week start", async () => {
|
it("should not update user associated with access token given invalid week start", async () => {
|
||||||
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
|
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" as any };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
|
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
|
||||||
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
|
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
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";
|
||||||
@@ -14,15 +15,15 @@ import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-co
|
|||||||
import { userSchemaResponse } from "@calcom/platform-types";
|
import { userSchemaResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/me",
|
path: "/v2/me",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@DocsTags("Me")
|
@DocsTags("Me")
|
||||||
export class MeController {
|
export class MeController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly schedulesRepository: SchedulesService
|
private readonly schedulesRepository: SchedulesService_2024_04_15
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { MeController } from "@/ee/me/me.controller";
|
import { MeController } from "@/ee/me/me.controller";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UsersModule, SchedulesModule, TokensModule],
|
imports: [UsersModule, SchedulesModule_2024_04_15, TokensModule],
|
||||||
controllers: [MeController],
|
controllers: [MeController],
|
||||||
})
|
})
|
||||||
export class MeModule {}
|
export class MeModule {}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
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";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
|
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||||
import { SlotsModule } from "@/modules/slots/slots.module";
|
import { SlotsModule } from "@/modules/slots/slots.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";
|
||||||
@@ -13,9 +15,11 @@ import { Module } from "@nestjs/common";
|
|||||||
imports: [
|
imports: [
|
||||||
GcalModule,
|
GcalModule,
|
||||||
ProviderModule,
|
ProviderModule,
|
||||||
SchedulesModule,
|
SchedulesModule_2024_04_15,
|
||||||
|
SchedulesModule_2024_06_11,
|
||||||
MeModule,
|
MeModule,
|
||||||
EventTypesModule,
|
EventTypesModule_2024_04_15,
|
||||||
|
EventTypesModule_2024_06_14,
|
||||||
CalendarsModule,
|
CalendarsModule,
|
||||||
BookingsModule,
|
BookingsModule,
|
||||||
SlotsModule,
|
SlotsModule,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
|
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
|
||||||
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 { 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 {
|
||||||
@@ -20,8 +21,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
|||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/provider",
|
path: "/v2/provider",
|
||||||
version: "2",
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsTags("Cal provider")
|
@DocsTags("Cal provider")
|
||||||
export class CalProviderController {
|
export class CalProviderController {
|
||||||
@@ -44,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
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller";
|
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
|
||||||
import { Module } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
|
||||||
providers: [SchedulesRepository, SchedulesService],
|
|
||||||
controllers: [SchedulesController],
|
|
||||||
exports: [SchedulesService, SchedulesRepository],
|
|
||||||
})
|
|
||||||
export class SchedulesModule {}
|
|
||||||
+19
-25
@@ -1,12 +1,11 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output";
|
||||||
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output";
|
||||||
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output";
|
||||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -17,10 +16,10 @@ 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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
|
||||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Schedules Endpoints", () => {
|
describe("Schedules Endpoints", () => {
|
||||||
describe("User Authentication", () => {
|
describe("User Authentication", () => {
|
||||||
@@ -32,23 +31,16 @@ describe("Schedules Endpoints", () => {
|
|||||||
const userEmail = "schedules-controller-e2e@api.com";
|
const userEmail = "schedules-controller-e2e@api.com";
|
||||||
let user: User;
|
let user: User;
|
||||||
|
|
||||||
let createdSchedule: CreateScheduleOutput["data"];
|
let createdSchedule: CreateScheduleOutput_2024_04_15["data"];
|
||||||
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
||||||
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
|
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
|
||||||
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: [
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_04_15],
|
||||||
AppModule,
|
|
||||||
PrismaModule,
|
|
||||||
AvailabilitiesModule,
|
|
||||||
UsersModule,
|
|
||||||
TokensModule,
|
|
||||||
SchedulesModule,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -79,7 +71,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
const scheduleTimeZone = "Europe/Rome";
|
const scheduleTimeZone = "Europe/Rome";
|
||||||
const isDefault = true;
|
const isDefault = true;
|
||||||
|
|
||||||
const body: CreateScheduleInput = {
|
const body: CreateScheduleInput_2024_04_15 = {
|
||||||
name: scheduleName,
|
name: scheduleName,
|
||||||
timeZone: scheduleTimeZone,
|
timeZone: scheduleTimeZone,
|
||||||
isDefault,
|
isDefault,
|
||||||
@@ -87,10 +79,11 @@ describe("Schedules Endpoints", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/api/v2/schedules")
|
.post("/api/v2/schedules")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseData: CreateScheduleOutput = response.body;
|
const responseData: CreateScheduleOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data.isDefault).toEqual(isDefault);
|
expect(responseData.data.isDefault).toEqual(isDefault);
|
||||||
@@ -117,7 +110,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
.get("/api/v2/schedules/default")
|
.get("/api/v2/schedules/default")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseData: CreateScheduleOutput = response.body;
|
const responseData: CreateScheduleOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data.id).toEqual(createdSchedule.id);
|
expect(responseData.data.id).toEqual(createdSchedule.id);
|
||||||
@@ -137,7 +130,7 @@ describe("Schedules Endpoints", () => {
|
|||||||
.get(`/api/v2/schedules`)
|
.get(`/api/v2/schedules`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseData: GetSchedulesOutput = response.body;
|
const responseData: GetSchedulesOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
|
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
|
||||||
@@ -155,16 +148,17 @@ describe("Schedules Endpoints", () => {
|
|||||||
it("should update schedule name", async () => {
|
it("should update schedule name", async () => {
|
||||||
const newScheduleName = "new-schedule-name";
|
const newScheduleName = "new-schedule-name";
|
||||||
|
|
||||||
const body: UpdateScheduleInput = {
|
const body: UpdateScheduleInput_2024_04_15 = {
|
||||||
name: newScheduleName,
|
name: newScheduleName,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_04_15)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
const responseData: UpdateScheduleOutput = response.body;
|
const responseData: UpdateScheduleOutput_2024_04_15 = response.body;
|
||||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseData.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseData.data.schedule.name).toEqual(newScheduleName);
|
expect(responseData.data.schedule.name).toEqual(newScheduleName);
|
||||||
+31
-24
@@ -1,13 +1,14 @@
|
|||||||
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output";
|
||||||
import { DeleteScheduleOutput } from "@/ee/schedules/outputs/delete-schedule.output";
|
import { DeleteScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/delete-schedule.output";
|
||||||
import { GetDefaultScheduleOutput } from "@/ee/schedules/outputs/get-default-schedule.output";
|
import { GetDefaultScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-default-schedule.output";
|
||||||
import { GetScheduleOutput } from "@/ee/schedules/outputs/get-schedule.output";
|
import { GetScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedule.output";
|
||||||
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output";
|
||||||
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
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 {
|
||||||
@@ -26,25 +27,25 @@ import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
|||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
|
||||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
|
||||||
import { CreateScheduleInput } from "../inputs/create-schedule.input";
|
import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "schedules",
|
path: "/v2/schedules",
|
||||||
version: "2",
|
version: VERSION_2024_04_15_VALUE,
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@DocsTags("Schedules")
|
@DocsTags("Schedules")
|
||||||
export class SchedulesController {
|
export class SchedulesController_2024_04_15 {
|
||||||
constructor(private readonly schedulesService: SchedulesService) {}
|
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
async createSchedule(
|
async createSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: CreateScheduleInput
|
@Body() bodySchedule: CreateScheduleInput_2024_04_15
|
||||||
): Promise<CreateScheduleOutput> {
|
): Promise<CreateScheduleOutput_2024_04_15> {
|
||||||
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||||
|
|
||||||
@@ -56,8 +57,14 @@ export class SchedulesController {
|
|||||||
|
|
||||||
@Get("/default")
|
@Get("/default")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
@ApiResponse({ status: 200, description: "Returns the default schedule", type: GetDefaultScheduleOutput })
|
@ApiResponse({
|
||||||
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetDefaultScheduleOutput | null> {
|
status: 200,
|
||||||
|
description: "Returns the default schedule",
|
||||||
|
type: GetDefaultScheduleOutput_2024_04_15,
|
||||||
|
})
|
||||||
|
async getDefaultSchedule(
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
|
||||||
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||||
const scheduleFormatted = schedule
|
const scheduleFormatted = schedule
|
||||||
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
||||||
@@ -75,7 +82,7 @@ export class SchedulesController {
|
|||||||
async getSchedule(
|
async getSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
): Promise<GetScheduleOutput> {
|
): Promise<GetScheduleOutput_2024_04_15> {
|
||||||
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||||
|
|
||||||
@@ -87,7 +94,7 @@ export class SchedulesController {
|
|||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput> {
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
|
||||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||||
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
||||||
|
|
||||||
@@ -102,9 +109,9 @@ export class SchedulesController {
|
|||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
async updateSchedule(
|
async updateSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: UpdateScheduleInput,
|
@Body() bodySchedule: UpdateScheduleInput_2024_04_15,
|
||||||
@Param("scheduleId") scheduleId: string
|
@Param("scheduleId") scheduleId: string
|
||||||
): Promise<UpdateScheduleOutput> {
|
): Promise<UpdateScheduleOutput_2024_04_15> {
|
||||||
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
||||||
user,
|
user,
|
||||||
Number(scheduleId),
|
Number(scheduleId),
|
||||||
@@ -123,7 +130,7 @@ export class SchedulesController {
|
|||||||
async deleteSchedule(
|
async deleteSchedule(
|
||||||
@GetUser("id") userId: number,
|
@GetUser("id") userId: number,
|
||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
): Promise<DeleteScheduleOutput> {
|
): Promise<DeleteScheduleOutput_2024_04_15> {
|
||||||
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
|||||||
import { Transform, TransformFnParams } from "class-transformer";
|
import { Transform, TransformFnParams } from "class-transformer";
|
||||||
import { IsArray, IsDate, IsNumber } from "class-validator";
|
import { IsArray, IsDate, IsNumber } from "class-validator";
|
||||||
|
|
||||||
export class CreateAvailabilityInput {
|
export class CreateAvailabilityInput_2024_04_15 {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsNumber({}, { each: true })
|
@IsNumber({}, { each: true })
|
||||||
@ApiProperty({ example: [1, 2] })
|
@ApiProperty({ example: [1, 2] })
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
export class CreateScheduleInput {
|
export class CreateScheduleInput_2024_04_15 {
|
||||||
@IsString()
|
@IsString()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ export class CreateScheduleInput {
|
|||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => CreateAvailabilityInput)
|
@Type(() => CreateAvailabilityInput_2024_04_15)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
availabilities?: CreateAvailabilityInput[];
|
availabilities?: CreateAvailabilityInput_2024_04_15[];
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isDefault!: boolean;
|
isDefault!: boolean;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.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";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
export class CreateScheduleOutput {
|
export class CreateScheduleOutput_2024_04_15 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { IsEnum } from "class-validator";
|
|||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
export class DeleteScheduleOutput {
|
export class DeleteScheduleOutput_2024_04_15 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
Loaded 100 of 673 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user