Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
37c02572de | ||
|
|
b82b0a33a4 | ||
|
|
b1bbbcee90 | ||
|
|
1a381695e5 | ||
|
|
8831ff13c7 | ||
|
|
64845c670d | ||
|
|
ba06c6c26a | ||
|
|
49df99cf92 | ||
|
|
0cb2e60d9b | ||
|
|
b3485b07bf | ||
|
|
01933e5c15 | ||
|
|
5f5cf92456 | ||
|
|
29ccc58806 | ||
|
|
26bd02bd94 | ||
|
|
04bf23ed69 |
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
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: All checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
merge_group:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
type-check:
|
||||||
|
name: Type check
|
||||||
|
uses: ./.github/workflows/check-types.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Linters
|
||||||
|
uses: ./.github/workflows/lint.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
unit-test:
|
||||||
|
name: Tests
|
||||||
|
uses: ./.github/workflows/unit-tests.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build-api-v1:
|
||||||
|
name: Production builds
|
||||||
|
uses: ./.github/workflows/api-v1-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build-api-v2:
|
||||||
|
name: Production builds
|
||||||
|
uses: ./.github/workflows/api-v2-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Production builds
|
||||||
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
integration-test:
|
||||||
|
name: Tests
|
||||||
|
needs: [lint, build, build-api-v1, build-api-v2]
|
||||||
|
uses: ./.github/workflows/integration-tests.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: Tests
|
||||||
|
needs: [lint, build, build-api-v1, build-api-v2]
|
||||||
|
uses: ./.github/workflows/e2e.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-app-store:
|
||||||
|
name: Tests
|
||||||
|
needs: [lint, build, build-api-v1, build-api-v2]
|
||||||
|
uses: ./.github/workflows/e2e-app-store.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed:
|
||||||
|
name: Tests
|
||||||
|
needs: [lint, build, build-api-v1, build-api-v2]
|
||||||
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed-react:
|
||||||
|
name: Tests
|
||||||
|
needs: [lint, build, build-api-v1, build-api-v2]
|
||||||
|
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:
|
||||||
|
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
|
if: always()
|
||||||
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
steps:
|
||||||
|
- name: fail if conditional jobs failed
|
||||||
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||||
|
run: exit 1
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -75,15 +75,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn test -- --integrationTestsOnly
|
run: yarn test -- --integrationTestsOnly
|
||||||
- name: Upload Test Results
|
# TODO: Generate test results so we can upload them
|
||||||
if: ${{ always() }}
|
# - name: Upload Test Results
|
||||||
uses: actions/upload-artifact@v4
|
# if: ${{ always() }}
|
||||||
with:
|
# uses: actions/upload-artifact@v4
|
||||||
name: test-results
|
# with:
|
||||||
path: test-results
|
# name: test-results
|
||||||
|
# path: test-results
|
||||||
@@ -4,7 +4,6 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
merge_group:
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -35,13 +34,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/check-types.yml
|
uses: ./.github/workflows/check-types.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
unit-test:
|
|
||||||
name: Tests
|
|
||||||
needs: [changes]
|
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/unit-tests.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Linters
|
name: Linters
|
||||||
needs: [changes]
|
needs: [changes]
|
||||||
@@ -49,71 +41,22 @@ jobs:
|
|||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build-api-v1:
|
unit-test:
|
||||||
name: Production builds
|
name: Tests
|
||||||
needs: [changes]
|
needs: [changes]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/api-v1-production-build.yml
|
uses: ./.github/workflows/unit-tests.yml
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
build-api-v2:
|
|
||||||
name: Production builds
|
|
||||||
needs: [changes]
|
|
||||||
if: ${{ 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]
|
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
integration-test:
|
integration-test:
|
||||||
name: Tests
|
name: Tests
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
needs: [changes]
|
||||||
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
|
||||||
|
|
||||||
e2e:
|
|
||||||
name: Tests
|
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/e2e.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
e2e-app-store:
|
|
||||||
name: Tests
|
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/e2e-app-store.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
e2e-embed:
|
|
||||||
name: Tests
|
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/e2e-embed.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
e2e-embed-react:
|
|
||||||
name: Tests
|
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
analyze:
|
|
||||||
name: Analyze Build
|
|
||||||
needs: [changes, build]
|
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [changes, lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
needs: [changes, lint, type-check, unit-test, integration-test]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -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,17 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
export function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
// reduce sample rate to 10% on production
|
||||||
|
tracesSampleRate: process.env.NODE_ENV !== "production" ? 1.0 : 0.1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -180,8 +180,10 @@ export async function handler(req: NextApiRequest) {
|
|||||||
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
|
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
|
||||||
|
|
||||||
const args: Prisma.BookingFindManyArgs = {};
|
const args: Prisma.BookingFindManyArgs = {};
|
||||||
args.take = take;
|
if (req.query.take && req.query.page) {
|
||||||
args.skip = skip;
|
args.take = take;
|
||||||
|
args.skip = skip;
|
||||||
|
}
|
||||||
args.include = {
|
args.include = {
|
||||||
attendees: true,
|
attendees: true,
|
||||||
user: true,
|
user: true,
|
||||||
|
|||||||
@@ -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,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-libraries": "*",
|
"@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 +40,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 +56,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", ""),
|
||||||
|
licenceKeyUrl: 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;
|
||||||
|
licenceKeyUrl: 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import { bootstrap } from "@/app";
|
|||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
|
import { Editable } from "@/ee/event-types/inputs/enums/editable";
|
||||||
|
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
||||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||||
@@ -20,10 +22,15 @@ 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 } from "@calcom/platform-constants";
|
||||||
import { EventTypesByViewer, EventTypesPublic } from "@calcom/platform-libraries";
|
import {
|
||||||
|
EventTypesByViewer,
|
||||||
|
EventTypesPublic,
|
||||||
|
eventTypeBookingFields,
|
||||||
|
eventTypeLocations,
|
||||||
|
} 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", () => {
|
||||||
@@ -72,7 +79,7 @@ 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],
|
||||||
@@ -197,10 +204,91 @@ 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 = {
|
||||||
|
locations,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer()).patch(`/api/v2/event-types/null`).send(body).expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update event type locations", async () => {
|
||||||
|
const locations = [{ type: "inPerson", address: "123 Main St" }];
|
||||||
|
|
||||||
|
const body: UpdateEventTypeInput = {
|
||||||
|
locations,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventType> = response.body;
|
||||||
|
const responseLocations = eventTypeLocations.parse(responseBody.data.locations);
|
||||||
|
expect(responseLocations).toBeDefined();
|
||||||
|
expect(responseLocations.length).toEqual(locations.length);
|
||||||
|
expect(responseLocations).toEqual(locations);
|
||||||
|
eventType.locations = responseLocations;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update event type bookingFields", async () => {
|
||||||
|
const bookingFieldName = "location-name";
|
||||||
|
const bookingFields = [
|
||||||
|
{
|
||||||
|
name: bookingFieldName,
|
||||||
|
type: BaseField.radio,
|
||||||
|
label: "Location",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Via Bari 10, Roma, 90119, Italy",
|
||||||
|
value: "Via Bari 10, Roma, 90119, Italy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Via Reale 28, Roma, 9001, Italy",
|
||||||
|
value: "Via Reale 28, Roma, 9001, Italy",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
id: "user",
|
||||||
|
type: "user",
|
||||||
|
label: "User",
|
||||||
|
fieldRequired: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
editable: Editable.user,
|
||||||
|
required: true,
|
||||||
|
placeholder: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const body: UpdateEventTypeInput = {
|
||||||
|
bookingFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventType> = response.body;
|
||||||
|
const responseBookingFields = eventTypeBookingFields.parse(responseBody.data.bookingFields);
|
||||||
|
expect(responseBookingFields).toBeDefined();
|
||||||
|
// note(Lauris): response bookingFields are already existing default bookingFields + the new one
|
||||||
|
const responseBookingField = responseBookingFields.find((field) => field.name === bookingFieldName);
|
||||||
|
expect(responseBookingField).toEqual(bookingFields[0]);
|
||||||
|
eventType.bookingFields = responseBookingFields;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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
|
// 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);
|
||||||
|
|
||||||
@@ -217,7 +305,7 @@ 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
|
// 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);
|
||||||
|
|
||||||
@@ -235,7 +323,7 @@ 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
|
// 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);
|
||||||
|
|
||||||
@@ -252,7 +340,7 @@ 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
|
// 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);
|
||||||
|
|
||||||
@@ -271,7 +359,7 @@ 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
|
// 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);
|
||||||
|
|
||||||
@@ -287,7 +375,7 @@ 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
|
// 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
|
import { EventTypeIdParams } from "@/ee/event-types/inputs/event-type-id.input";
|
||||||
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||||
@@ -9,9 +10,10 @@ import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-ty
|
|||||||
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
||||||
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
import { EventTypesService } from "@/ee/event-types/services/event-types.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 { 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,17 +31,18 @@ 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: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Event types")
|
@DocsTags("Event types")
|
||||||
@@ -51,7 +54,7 @@ export class EventTypesController {
|
|||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
async createEventType(
|
async createEventType(
|
||||||
@Body() body: CreateEventTypeInput,
|
@Body() body: CreateEventTypeInput,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
@@ -66,9 +69,11 @@ 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() params: EventTypeIdParams,
|
||||||
|
@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 +90,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,
|
||||||
@@ -141,10 +146,11 @@ 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,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Body() body: UpdateEventTypeInput,
|
@Body() body: UpdateEventTypeInput,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<UpdateEventTypeOutput> {
|
): Promise<UpdateEventTypeOutput> {
|
||||||
@@ -158,9 +164,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,
|
||||||
|
@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);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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()
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsNumberString } from "class-validator";
|
||||||
|
|
||||||
|
export class EventTypeIdParams {
|
||||||
|
@IsNumberString()
|
||||||
|
eventTypeId!: number;
|
||||||
|
}
|
||||||
@@ -275,11 +275,11 @@ export class UpdateEventTypeInput {
|
|||||||
// @IsOptional()
|
// @IsOptional()
|
||||||
// parentId?: number;
|
// parentId?: number;
|
||||||
|
|
||||||
// @IsOptional()
|
@IsOptional()
|
||||||
// @IsArray()
|
@IsArray()
|
||||||
// @ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
// @Type(() => BookingField)
|
@Type(() => BookingField)
|
||||||
// bookingFields?: BookingField[];
|
bookingFields?: BookingField[];
|
||||||
|
|
||||||
// @IsString()
|
// @IsString()
|
||||||
// @IsOptional()
|
// @IsOptional()
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { SelectedCalendarsRepository } from "@/modules/selected-calendars/select
|
|||||||
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()
|
||||||
|
|||||||
@@ -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 {}
|
||||||
@@ -4,7 +4,8 @@ import { EventTypesModule } from "@/ee/event-types/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,7 +14,8 @@ import { Module } from "@nestjs/common";
|
|||||||
imports: [
|
imports: [
|
||||||
GcalModule,
|
GcalModule,
|
||||||
ProviderModule,
|
ProviderModule,
|
||||||
SchedulesModule,
|
SchedulesModule_2024_04_15,
|
||||||
|
SchedulesModule_2024_06_11,
|
||||||
MeModule,
|
MeModule,
|
||||||
EventTypesModule,
|
EventTypesModule,
|
||||||
CalendarsModule,
|
CalendarsModule,
|
||||||
|
|||||||
@@ -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;
|
||||||
+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 GetDefaultScheduleOutput {
|
export class GetDefaultScheduleOutput_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;
|
||||||
+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 GetScheduleOutput {
|
export class GetScheduleOutput_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;
|
||||||
+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 { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsArray, 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 GetSchedulesOutput {
|
export class GetSchedulesOutput_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;
|
||||||
+10
-10
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
||||||
|
|
||||||
class EventTypeModel {
|
class EventTypeModel_2024_04_15 {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ class EventTypeModel {
|
|||||||
eventName?: string | null;
|
eventName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvailabilityModel {
|
class AvailabilityModel_2024_04_15 {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ class AvailabilityModel {
|
|||||||
date?: Date | null;
|
date?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduleModel {
|
class ScheduleModel_2024_04_15 {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@@ -62,21 +62,21 @@ class ScheduleModel {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => EventTypeModel)
|
@Type(() => EventTypeModel_2024_04_15)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
eventType?: EventTypeModel[];
|
eventType?: EventTypeModel_2024_04_15[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => AvailabilityModel)
|
@Type(() => AvailabilityModel_2024_04_15)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
availability?: AvailabilityModel[];
|
availability?: AvailabilityModel_2024_04_15[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdatedScheduleOutput {
|
export class UpdatedScheduleOutput_2024_04_15 {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => ScheduleModel)
|
@Type(() => ScheduleModel_2024_04_15)
|
||||||
schedule!: ScheduleModel;
|
schedule!: ScheduleModel_2024_04_15;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isDefault!: boolean;
|
isDefault!: boolean;
|
||||||
File renamed without changes.
@@ -0,0 +1,20 @@
|
|||||||
|
import { UpdatedScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule-updated.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UpdateScheduleOutput_2024_04_15 {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: UpdatedScheduleOutput_2024_04_15,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => UpdatedScheduleOutput_2024_04_15)
|
||||||
|
data!: UpdatedScheduleOutput_2024_04_15;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { SchedulesController_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/controllers/schedules.controller";
|
||||||
|
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
|
||||||
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
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, UsersModule, TokensModule],
|
||||||
|
providers: [SchedulesRepository_2024_04_15, SchedulesService_2024_04_15],
|
||||||
|
controllers: [SchedulesController_2024_04_15],
|
||||||
|
exports: [SchedulesService_2024_04_15, SchedulesRepository_2024_04_15],
|
||||||
|
})
|
||||||
|
export class SchedulesModule_2024_04_15 {}
|
||||||
+5
-5
@@ -1,18 +1,18 @@
|
|||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.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 { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesRepository {
|
export class SchedulesRepository_2024_04_15 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async createScheduleWithAvailabilities(
|
async createScheduleWithAvailabilities(
|
||||||
userId: number,
|
userId: number,
|
||||||
schedule: CreateScheduleInput,
|
schedule: CreateScheduleInput_2024_04_15,
|
||||||
availabilities: CreateAvailabilityInput[]
|
availabilities: CreateAvailabilityInput_2024_04_15[]
|
||||||
) {
|
) {
|
||||||
const createScheduleData: Prisma.ScheduleCreateInput = {
|
const createScheduleData: Prisma.ScheduleCreateInput = {
|
||||||
user: {
|
user: {
|
||||||
+28
-14
@@ -1,26 +1,25 @@
|
|||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service";
|
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.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 { Schedule } from "@prisma/client";
|
import { Schedule } from "@prisma/client";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries";
|
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
import { updateScheduleHandler } from "@calcom/platform-libraries-0.0.2";
|
||||||
import {
|
import {
|
||||||
transformWorkingHoursForClient,
|
transformWorkingHoursForClient,
|
||||||
transformAvailabilityForClient,
|
transformAvailabilityForClient,
|
||||||
transformDateOverridesForClient,
|
transformDateOverridesForClient,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries-0.0.2";
|
||||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesService {
|
export class SchedulesService_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly schedulesRepository: SchedulesRepository,
|
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
|
||||||
private readonly availabilitiesService: AvailabilitiesService,
|
|
||||||
private readonly usersRepository: UsersRepository
|
private readonly usersRepository: UsersRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -34,10 +33,10 @@ export class SchedulesService {
|
|||||||
return this.createUserSchedule(userId, schedule);
|
return this.createUserSchedule(userId, schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUserSchedule(userId: number, schedule: CreateScheduleInput) {
|
async createUserSchedule(userId: number, schedule: CreateScheduleInput_2024_04_15) {
|
||||||
const availabilities = schedule.availabilities?.length
|
const availabilities = schedule.availabilities?.length
|
||||||
? schedule.availabilities
|
? schedule.availabilities
|
||||||
: [this.availabilitiesService.getDefaultAvailabilityInput()];
|
: [this.getDefaultAvailabilityInput()];
|
||||||
|
|
||||||
const createdSchedule = await this.schedulesRepository.createScheduleWithAvailabilities(
|
const createdSchedule = await this.schedulesRepository.createScheduleWithAvailabilities(
|
||||||
userId,
|
userId,
|
||||||
@@ -76,7 +75,11 @@ export class SchedulesService {
|
|||||||
return this.schedulesRepository.getSchedulesByUserId(userId);
|
return this.schedulesRepository.getSchedulesByUserId(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserSchedule(user: UserWithProfile, scheduleId: number, bodySchedule: UpdateScheduleInput) {
|
async updateUserSchedule(
|
||||||
|
user: UserWithProfile,
|
||||||
|
scheduleId: number,
|
||||||
|
bodySchedule: UpdateScheduleInput_2024_04_15
|
||||||
|
) {
|
||||||
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
if (!existingSchedule) {
|
if (!existingSchedule) {
|
||||||
@@ -158,4 +161,15 @@ export class SchedulesService {
|
|||||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDefaultAvailabilityInput(): CreateAvailabilityInput_2024_04_15 {
|
||||||
|
const startTime = new Date(new Date().setUTCHours(9, 0, 0, 0));
|
||||||
|
const endTime = new Date(new Date().setUTCHours(17, 0, 0, 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
days: [1, 2, 3, 4, 5],
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.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_11 } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
CreateScheduleInput_2024_06_11,
|
||||||
|
CreateScheduleOutput_2024_06_11,
|
||||||
|
GetScheduleOutput_2024_06_11,
|
||||||
|
GetSchedulesOutput_2024_06_11,
|
||||||
|
ScheduleOutput_2024_06_11,
|
||||||
|
UpdateScheduleOutput_2024_06_11,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
import { UpdateScheduleInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
describe("Schedules Endpoints", () => {
|
||||||
|
describe("User Authentication", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "schedules-controller-e2e@api.com";
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
const createScheduleInput: CreateScheduleInput_2024_06_11 = {
|
||||||
|
name: "work",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultAvailability: CreateScheduleInput_2024_06_11["availability"] = [
|
||||||
|
{
|
||||||
|
days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||||
|
startTime: "09:00",
|
||||||
|
endTime: "17:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let createdSchedule: CreateScheduleOutput_2024_06_11["data"];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule_2024_06_11],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a default schedule", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/schedules")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.send(createScheduleInput)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
createdSchedule = response.body.data;
|
||||||
|
|
||||||
|
const expectedSchedule = {
|
||||||
|
...createScheduleInput,
|
||||||
|
availability: defaultAvailability,
|
||||||
|
overrides: [],
|
||||||
|
};
|
||||||
|
outputScheduleMatchesExpected(createdSchedule, expectedSchedule, 1);
|
||||||
|
|
||||||
|
const scheduleOwner = createdSchedule.ownerId
|
||||||
|
? await userRepositoryFixture.get(createdSchedule.ownerId)
|
||||||
|
: null;
|
||||||
|
expect(scheduleOwner?.defaultScheduleId).toEqual(createdSchedule.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function outputScheduleMatchesExpected(
|
||||||
|
outputSchedule: ScheduleOutput_2024_06_11 | null,
|
||||||
|
expected: CreateScheduleInput_2024_06_11 & {
|
||||||
|
availability: CreateScheduleInput_2024_06_11["availability"];
|
||||||
|
} & {
|
||||||
|
overrides: CreateScheduleInput_2024_06_11["overrides"];
|
||||||
|
},
|
||||||
|
expectedAvailabilityLength: number
|
||||||
|
) {
|
||||||
|
expect(outputSchedule).toBeTruthy();
|
||||||
|
expect(outputSchedule?.name).toEqual(expected.name);
|
||||||
|
expect(outputSchedule?.timeZone).toEqual(expected.timeZone);
|
||||||
|
expect(outputSchedule?.isDefault).toEqual(expected.isDefault);
|
||||||
|
expect(outputSchedule?.availability.length).toEqual(expectedAvailabilityLength);
|
||||||
|
|
||||||
|
const outputScheduleAvailability = outputSchedule?.availability[0];
|
||||||
|
expect(outputScheduleAvailability).toBeDefined();
|
||||||
|
expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days);
|
||||||
|
expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime);
|
||||||
|
expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime);
|
||||||
|
|
||||||
|
expect(JSON.stringify(outputSchedule?.overrides)).toEqual(JSON.stringify(expected.overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should get default schedule", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get("/api/v2/schedules/default")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const outputSchedule = responseBody.data;
|
||||||
|
|
||||||
|
const expectedSchedule = {
|
||||||
|
...createScheduleInput,
|
||||||
|
availability: defaultAvailability,
|
||||||
|
overrides: [],
|
||||||
|
};
|
||||||
|
outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get schedules", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/schedules`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const outputSchedule = responseBody.data[0];
|
||||||
|
|
||||||
|
const expectedSchedule = {
|
||||||
|
...createScheduleInput,
|
||||||
|
availability: defaultAvailability,
|
||||||
|
overrides: [],
|
||||||
|
};
|
||||||
|
outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update schedule name", async () => {
|
||||||
|
const newScheduleName = "updated-schedule-name";
|
||||||
|
|
||||||
|
const body: UpdateScheduleInput_2024_06_11 = {
|
||||||
|
name: newScheduleName,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then((response: any) => {
|
||||||
|
const responseData: UpdateScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const responseSchedule = responseData.data;
|
||||||
|
|
||||||
|
const expectedSchedule = { ...createdSchedule, name: newScheduleName };
|
||||||
|
outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1);
|
||||||
|
|
||||||
|
createdSchedule = responseSchedule;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add overrides", async () => {
|
||||||
|
const overrides = [
|
||||||
|
{
|
||||||
|
date: "2026-05-05",
|
||||||
|
startTime: "10:00",
|
||||||
|
endTime: "12:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const body: UpdateScheduleInput_2024_06_11 = {
|
||||||
|
overrides,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then((response: any) => {
|
||||||
|
const responseData: UpdateScheduleOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const responseSchedule = responseData.data;
|
||||||
|
|
||||||
|
const expectedSchedule = { ...createdSchedule, overrides };
|
||||||
|
outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1);
|
||||||
|
|
||||||
|
createdSchedule = responseSchedule;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should delete schedule", async () => {
|
||||||
|
return request(app.getHttpServer()).delete(`/api/v2/schedules/${createdSchedule.id}`).expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
try {
|
||||||
|
await scheduleRepositoryFixture.deleteById(createdSchedule.id);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||||
|
import { VERSION_2024_06_11_VALUE } from "@/lib/api-versions";
|
||||||
|
import { 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 {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
|
||||||
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
CreateScheduleOutput_2024_06_11,
|
||||||
|
CreateScheduleInput_2024_06_11,
|
||||||
|
UpdateScheduleInput_2024_06_11,
|
||||||
|
GetScheduleOutput_2024_06_11,
|
||||||
|
UpdateScheduleOutput_2024_06_11,
|
||||||
|
GetDefaultScheduleOutput_2024_06_11,
|
||||||
|
DeleteScheduleOutput_2024_06_11,
|
||||||
|
GetSchedulesOutput_2024_06_11,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/schedules",
|
||||||
|
version: VERSION_2024_06_11_VALUE,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
|
@DocsTags("Schedules")
|
||||||
|
export class SchedulesController_2024_06_11 {
|
||||||
|
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
|
||||||
|
|
||||||
|
@Post("/")
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
async createSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Body() bodySchedule: CreateScheduleInput_2024_06_11
|
||||||
|
): Promise<CreateScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/default")
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Returns the default schedule",
|
||||||
|
type: GetDefaultScheduleOutput_2024_06_11,
|
||||||
|
})
|
||||||
|
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:scheduleId")
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
@Throttle({ default: { limit: 10, ttl: 60000 } }) // allow 10 requests per minute (for :scheduleId)
|
||||||
|
async getSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("scheduleId") scheduleId: number
|
||||||
|
): Promise<GetScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
|
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/:scheduleId")
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
async updateSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Body() bodySchedule: UpdateScheduleInput_2024_06_11,
|
||||||
|
@Param("scheduleId") scheduleId: string
|
||||||
|
): Promise<UpdateScheduleOutput_2024_06_11> {
|
||||||
|
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
||||||
|
user.id,
|
||||||
|
Number(scheduleId),
|
||||||
|
bodySchedule
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: updatedSchedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("/:scheduleId")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
async deleteSchedule(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("scheduleId") scheduleId: number
|
||||||
|
): Promise<DeleteScheduleOutput_2024_06_11> {
|
||||||
|
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SchedulesController_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/controllers/schedules.controller";
|
||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||||
|
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, UsersModule, TokensModule],
|
||||||
|
providers: [
|
||||||
|
SchedulesRepository_2024_06_11,
|
||||||
|
SchedulesService_2024_06_11,
|
||||||
|
InputSchedulesService_2024_06_11,
|
||||||
|
OutputSchedulesService_2024_06_11,
|
||||||
|
],
|
||||||
|
controllers: [SchedulesController_2024_06_11],
|
||||||
|
exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11],
|
||||||
|
})
|
||||||
|
export class SchedulesModule_2024_06_11 {}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
import type { CreateScheduleInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
type InputScheduleAvailabilityTransformed = {
|
||||||
|
days: number[];
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputScheduleOverrideTransformed = {
|
||||||
|
date: Date;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputScheduleTransformed = Omit<CreateScheduleInput_2024_06_11, "availability" | "overrides"> & {
|
||||||
|
availability: InputScheduleAvailabilityTransformed[];
|
||||||
|
overrides: InputScheduleOverrideTransformed[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SchedulesRepository_2024_06_11 {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async createSchedule(userId: number, schedule: Omit<InputScheduleTransformed, "isDefault">) {
|
||||||
|
const { availability, overrides } = schedule;
|
||||||
|
|
||||||
|
const createScheduleData: Prisma.ScheduleCreateInput = {
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: schedule.name,
|
||||||
|
timeZone: schedule.timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
const availabilitiesAndOverrides: Prisma.AvailabilityCreateManyInput[] = [];
|
||||||
|
|
||||||
|
if (availability && availability.length > 0) {
|
||||||
|
availability.forEach((availability) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
days: availability.days,
|
||||||
|
startTime: availability.startTime,
|
||||||
|
endTime: availability.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides && overrides.length > 0) {
|
||||||
|
overrides.forEach((override) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
date: override.date,
|
||||||
|
startTime: override.startTime,
|
||||||
|
endTime: override.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availabilitiesAndOverrides.length > 0) {
|
||||||
|
createScheduleData.availability = {
|
||||||
|
createMany: {
|
||||||
|
data: availabilitiesAndOverrides,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdSchedule = await this.dbWrite.prisma.schedule.create({
|
||||||
|
data: {
|
||||||
|
...createScheduleData,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getScheduleById(scheduleId: number) {
|
||||||
|
const schedule = await this.dbRead.prisma.schedule.findUnique({
|
||||||
|
where: {
|
||||||
|
id: scheduleId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSchedule(
|
||||||
|
userId: number,
|
||||||
|
scheduleId: number,
|
||||||
|
schedule: Partial<Omit<InputScheduleTransformed, "isDefault">>
|
||||||
|
) {
|
||||||
|
const { availability, overrides } = schedule;
|
||||||
|
|
||||||
|
const updateScheduleData: Prisma.ScheduleUpdateInput = {
|
||||||
|
name: schedule.name,
|
||||||
|
timeZone: schedule.timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
const availabilitiesAndOverrides: Prisma.AvailabilityCreateManyInput[] = [];
|
||||||
|
|
||||||
|
const deleteConditions = [];
|
||||||
|
if (availability) {
|
||||||
|
// note(Lauris): availabilities and overrides are stored in the same "Availability" table,
|
||||||
|
// but availabilities have "date" field as null, while overrides have it as not null, so delete
|
||||||
|
// condition below results in deleting only rows from Availability table that are availabilities.
|
||||||
|
deleteConditions.push({
|
||||||
|
scheduleId: { equals: scheduleId },
|
||||||
|
date: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides) {
|
||||||
|
// note(Lauris): availabilities and overrides are stored in the same "Availability" table,
|
||||||
|
// but overrides have "date" field as not-null, while availabilities have it as null, so delete
|
||||||
|
// condition below results in deleting only rows from Availability table that are overrides.
|
||||||
|
deleteConditions.push({
|
||||||
|
scheduleId: { equals: scheduleId },
|
||||||
|
NOT: { date: null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availability && availability.length > 0) {
|
||||||
|
availability.forEach((availability) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
days: availability.days,
|
||||||
|
startTime: availability.startTime,
|
||||||
|
endTime: availability.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides && overrides.length > 0) {
|
||||||
|
overrides.forEach((override) => {
|
||||||
|
availabilitiesAndOverrides.push({
|
||||||
|
date: override.date,
|
||||||
|
startTime: override.startTime,
|
||||||
|
endTime: override.endTime,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availabilitiesAndOverrides.length > 0) {
|
||||||
|
updateScheduleData.availability = {
|
||||||
|
deleteMany: deleteConditions,
|
||||||
|
createMany: {
|
||||||
|
data: availabilitiesAndOverrides,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSchedule = await this.dbWrite.prisma.schedule.update({
|
||||||
|
where: {
|
||||||
|
id: scheduleId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...updateScheduleData,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSchedulesByUserId(userId: number) {
|
||||||
|
const schedules = await this.dbRead.prisma.schedule.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return schedules;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteScheduleById(scheduleId: number) {
|
||||||
|
return this.dbWrite.prisma.schedule.delete({
|
||||||
|
where: {
|
||||||
|
id: scheduleId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserSchedulesCount(userId: number) {
|
||||||
|
return this.dbRead.prisma.schedule.count({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
transformApiScheduleOverrides,
|
||||||
|
transformApiScheduleAvailability,
|
||||||
|
} from "@calcom/platform-libraries-0.0.4";
|
||||||
|
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InputSchedulesService_2024_06_11 {
|
||||||
|
transformInputCreateSchedule(inputSchedule: CreateScheduleInput_2024_06_11) {
|
||||||
|
const defaultAvailability: ScheduleAvailabilityInput_2024_06_11[] = [
|
||||||
|
{
|
||||||
|
days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||||
|
startTime: "09:00",
|
||||||
|
endTime: "17:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const defaultOverrides: ScheduleOverrideInput_2024_06_11[] = [];
|
||||||
|
|
||||||
|
const availability = this.transformInputScheduleAvailability(
|
||||||
|
inputSchedule.availability || defaultAvailability
|
||||||
|
);
|
||||||
|
const overrides = this.transformInputOverrides(inputSchedule.overrides || defaultOverrides);
|
||||||
|
|
||||||
|
const internalCreateSchedule = {
|
||||||
|
...inputSchedule,
|
||||||
|
availability,
|
||||||
|
overrides,
|
||||||
|
};
|
||||||
|
|
||||||
|
return internalCreateSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputScheduleAvailability(inputAvailability: ScheduleAvailabilityInput_2024_06_11[]) {
|
||||||
|
return transformApiScheduleAvailability(inputAvailability);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputOverrides(inputOverrides: ScheduleOverrideInput_2024_06_11[]) {
|
||||||
|
return transformApiScheduleOverrides(inputOverrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import type { Availability, Schedule } from "@prisma/client";
|
||||||
|
|
||||||
|
import { WeekDay } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OutputSchedulesService_2024_06_11 {
|
||||||
|
constructor(private readonly usersRepository: UsersRepository) {}
|
||||||
|
|
||||||
|
async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) {
|
||||||
|
if (!databaseSchedule.timeZone) {
|
||||||
|
throw new Error("Failed to create schedule because its timezone is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId(
|
||||||
|
databaseSchedule.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
const createdScheduleAvailabilities = databaseSchedule.availability.filter(
|
||||||
|
(availability) => !!availability.days.length
|
||||||
|
);
|
||||||
|
const createdScheduleOverrides = databaseSchedule.availability.filter((override) => !!override.date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: databaseSchedule.id,
|
||||||
|
ownerId: databaseSchedule.userId,
|
||||||
|
name: databaseSchedule.name,
|
||||||
|
timeZone: databaseSchedule.timeZone,
|
||||||
|
availability: createdScheduleAvailabilities.map((availability) => ({
|
||||||
|
days: availability.days.map(transformNumberToDay),
|
||||||
|
startTime: this.padHoursMinutesWithZeros(
|
||||||
|
availability.startTime.getUTCHours() + ":" + availability.startTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
endTime: this.padHoursMinutesWithZeros(
|
||||||
|
availability.endTime.getUTCHours() + ":" + availability.endTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
isDefault: databaseSchedule.id === ownerDefaultScheduleId,
|
||||||
|
overrides: createdScheduleOverrides.map((override) => ({
|
||||||
|
date:
|
||||||
|
override.date?.getUTCFullYear() +
|
||||||
|
"-" +
|
||||||
|
(override.date ? override.date.getUTCMonth() + 1 : "").toString().padStart(2, "0") +
|
||||||
|
"-" +
|
||||||
|
override.date?.getUTCDate().toString().padStart(2, "0"),
|
||||||
|
startTime: this.padHoursMinutesWithZeros(
|
||||||
|
override.startTime.getUTCHours() + ":" + override.startTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
endTime: this.padHoursMinutesWithZeros(
|
||||||
|
override.endTime.getUTCHours() + ":" + override.endTime.getUTCMinutes()
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
padHoursMinutesWithZeros(hhMM: string) {
|
||||||
|
const [hours, minutes] = hhMM.split(":");
|
||||||
|
|
||||||
|
const formattedHours = hours.padStart(2, "0");
|
||||||
|
const formattedMinutes = minutes.padStart(2, "0");
|
||||||
|
|
||||||
|
return `${formattedHours}:${formattedMinutes}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformNumberToDay(day: number): WeekDay {
|
||||||
|
const weekMap: { [key: number]: WeekDay } = {
|
||||||
|
0: "Sunday",
|
||||||
|
1: "Monday",
|
||||||
|
2: "Tuesday",
|
||||||
|
3: "Wednesday",
|
||||||
|
4: "Thursday",
|
||||||
|
5: "Friday",
|
||||||
|
6: "Saturday",
|
||||||
|
};
|
||||||
|
return weekMap[day];
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { Schedule } from "@prisma/client";
|
||||||
|
|
||||||
|
import { CreateScheduleInput_2024_06_11, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
import { UpdateScheduleInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SchedulesService_2024_06_11 {
|
||||||
|
constructor(
|
||||||
|
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||||
|
private readonly inputSchedulesService: InputSchedulesService_2024_06_11,
|
||||||
|
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
||||||
|
private readonly usersRepository: UsersRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createUserDefaultSchedule(userId: number, timeZone: string) {
|
||||||
|
const defaultSchedule = {
|
||||||
|
isDefault: true,
|
||||||
|
name: "Default schedule",
|
||||||
|
timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.createUserSchedule(userId, defaultSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUserSchedule(
|
||||||
|
userId: number,
|
||||||
|
scheduleInput: CreateScheduleInput_2024_06_11
|
||||||
|
): Promise<ScheduleOutput_2024_06_11> {
|
||||||
|
const schedule = this.inputSchedulesService.transformInputCreateSchedule(scheduleInput);
|
||||||
|
|
||||||
|
const createdSchedule = await this.schedulesRepository.createSchedule(userId, schedule);
|
||||||
|
|
||||||
|
if (schedule.isDefault) {
|
||||||
|
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(createdSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserScheduleDefault(userId: number) {
|
||||||
|
const user = await this.usersRepository.findById(userId);
|
||||||
|
|
||||||
|
if (!user?.defaultScheduleId) return null;
|
||||||
|
|
||||||
|
const defaultSchedule = await this.schedulesRepository.getScheduleById(user.defaultScheduleId);
|
||||||
|
|
||||||
|
if (!defaultSchedule) return null;
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(defaultSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserSchedule(userId: number, scheduleId: number) {
|
||||||
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
|
if (!existingSchedule) {
|
||||||
|
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsSchedule(userId, existingSchedule);
|
||||||
|
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(existingSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserSchedules(userId: number) {
|
||||||
|
const schedules = await this.schedulesRepository.getSchedulesByUserId(userId);
|
||||||
|
return Promise.all(
|
||||||
|
schedules.map(async (schedule) => {
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(schedule);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserSchedule(userId: number, scheduleId: number, bodySchedule: UpdateScheduleInput_2024_06_11) {
|
||||||
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
|
if (!existingSchedule) {
|
||||||
|
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsSchedule(userId, existingSchedule);
|
||||||
|
|
||||||
|
const availability = bodySchedule.availability
|
||||||
|
? this.inputSchedulesService.transformInputScheduleAvailability(bodySchedule.availability)
|
||||||
|
: undefined;
|
||||||
|
const overrides = bodySchedule.overrides
|
||||||
|
? this.inputSchedulesService.transformInputOverrides(bodySchedule.overrides)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (bodySchedule.isDefault) {
|
||||||
|
await this.usersRepository.setDefaultSchedule(userId, scheduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSchedule = await this.schedulesRepository.updateSchedule(userId, scheduleId, {
|
||||||
|
...bodySchedule,
|
||||||
|
availability,
|
||||||
|
overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.outputSchedulesService.getResponseSchedule(updatedSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUserSchedule(userId: number, scheduleId: number) {
|
||||||
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
|
if (!existingSchedule) {
|
||||||
|
throw new BadRequestException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsSchedule(userId, existingSchedule);
|
||||||
|
|
||||||
|
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
||||||
|
if (userId !== schedule.userId) {
|
||||||
|
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,10 @@ export type Environment = {
|
|||||||
STRIPE_API_KEY: string;
|
STRIPE_API_KEY: string;
|
||||||
STRIPE_WEBHOOK_SECRET: string;
|
STRIPE_WEBHOOK_SECRET: string;
|
||||||
WEB_APP_URL: string;
|
WEB_APP_URL: string;
|
||||||
|
IS_E2E: boolean;
|
||||||
|
CALCOM_LICENSE_KEY: string;
|
||||||
|
GET_LICENSE_KEY_URL: string;
|
||||||
|
API_KEY_PREFIX: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
|
|
||||||
import { BaseExceptionFilter } from "@nestjs/core";
|
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import { Request } from "express";
|
|
||||||
|
|
||||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
|
||||||
import { Response } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Catch()
|
|
||||||
export class SentryFilter extends BaseExceptionFilter {
|
|
||||||
private readonly logger = new Logger("SentryExceptionFilter");
|
|
||||||
|
|
||||||
handleUnknownError(exception: any, host: ArgumentsHost): void {
|
|
||||||
const ctx = host.switchToHttp();
|
|
||||||
const response = ctx.getResponse<Response>();
|
|
||||||
const request = ctx.getRequest<Request>();
|
|
||||||
const requestId = request.headers["X-Request-Id"];
|
|
||||||
|
|
||||||
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
|
|
||||||
exception,
|
|
||||||
body: request.body,
|
|
||||||
headers: request.headers,
|
|
||||||
url: request.url,
|
|
||||||
method: request.method,
|
|
||||||
requestId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// capture if client has been init
|
|
||||||
if (Boolean(Sentry.getCurrentHub().getClient())) {
|
|
||||||
Sentry.captureException(exception);
|
|
||||||
}
|
|
||||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
|
||||||
status: ERROR_STATUS,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
path: request.url,
|
|
||||||
error: { code: INTERNAL_SERVER_ERROR, message: "Internal server error." },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
|||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { TRPCError } from "@calcom/platform-libraries";
|
import { TRPCError } from "@calcom/platform-libraries-0.0.2";
|
||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Catch(TRPCError)
|
@Catch(TRPCError)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { getEnv } from "@/env";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
|
if (process.env.SENTRY_DSN) {
|
||||||
|
// Ensure to call this before requiring any other modules!
|
||||||
|
Sentry.init({
|
||||||
|
dsn: getEnv("SENTRY_DSN"),
|
||||||
|
// Add Performance Monitoring by setting tracesSampleRate
|
||||||
|
// We recommend adjusting this value in production
|
||||||
|
// todo: Evaluate? tracesSampleRate: 1.0
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
|
||||||
export const hashAPIKey = (apiKey: string): string => createHash("sha256").update(apiKey).digest("hex");
|
export const hashAPIKey = (apiKey: string): string => createHash("sha256").update(apiKey).digest("hex");
|
||||||
|
|
||||||
|
export const isApiKey = (authString: string, prefix: string): boolean =>
|
||||||
|
authString?.startsWith(prefix ?? "cal_");
|
||||||
|
|
||||||
|
export const stripApiKey = (apiKey: string, prefix?: string): string => apiKey.replace(prefix ?? "cal_", "");
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { VersionValue } from "@nestjs/common/interfaces";
|
||||||
|
|
||||||
|
import { API_VERSIONS, VERSION_2024_04_15, VERSION_2024_06_11 } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue;
|
||||||
|
export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue;
|
||||||
|
export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
import { IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
import { CapitalizeTimeZone } from "./capitalize-timezone";
|
||||||
|
|
||||||
|
class TestDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@CapitalizeTimeZone()
|
||||||
|
timeZone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CapitalizeTimeZone", () => {
|
||||||
|
it("should capitalize single part time zone correctly", () => {
|
||||||
|
const input = { timeZone: "egypt" };
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBe("Egypt");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should capitalize one-part time zone correctly", () => {
|
||||||
|
const input = { timeZone: "europe/rome" };
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBe("Europe/Rome");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should capitalize multi-part time zone correctly", () => {
|
||||||
|
const input = { timeZone: "america/new_york" };
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBe("America/New_York");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should capitalize complex time zone correctly", () => {
|
||||||
|
const input = { timeZone: "europe/isle_of_man" };
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBe("Europe/Isle_Of_Man");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle already capitalized time zones correctly", () => {
|
||||||
|
const input = { timeZone: "Asia/Tokyo" };
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBe("Asia/Tokyo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle missing time zone correctly", () => {
|
||||||
|
const input = {};
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should capitalize EST at the end of the string", () => {
|
||||||
|
const input = { email: "test@example.com", timeZone: "utc/est" };
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBe("UTC/EST");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should capitalize UTC when surrounded by non-alphabetical characters", () => {
|
||||||
|
const input = { email: "test@example.com", timeZone: "utc/gmt+3_est" };
|
||||||
|
const output = plainToClass(TestDto, input);
|
||||||
|
expect(output.timeZone).toBe("UTC/GMT+3_EST");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Transform } from "class-transformer";
|
||||||
|
|
||||||
|
export function CapitalizeTimeZone(): PropertyDecorator {
|
||||||
|
return Transform(({ value }) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const parts = value.split("/");
|
||||||
|
const normalizedParts = parts.map((part) =>
|
||||||
|
part
|
||||||
|
.split("_")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join("_")
|
||||||
|
);
|
||||||
|
let normalizedTimeZone = normalizedParts.join("/");
|
||||||
|
|
||||||
|
// note(Lauris): regex matching GMT, EST, UTC at the start, end, or surrounded by non-letters and capitalizing them
|
||||||
|
const specialCases = ["GMT", "EST", "UTC"];
|
||||||
|
specialCases.forEach((specialCase) => {
|
||||||
|
const regex = new RegExp(`(^|[^a-zA-Z])(${specialCase})([^a-zA-Z]|$)`, "gi");
|
||||||
|
normalizedTimeZone = normalizedTimeZone.replace(regex, (match, p1, p2, p3) => {
|
||||||
|
return `${p1}${specialCase}${p3}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return normalizedTimeZone;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getApiKeyFromHash(hashedKey: string) {
|
||||||
|
return this.dbRead.prisma.apiKey.findUnique({
|
||||||
|
where: {
|
||||||
|
hashedKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ApiKeyService } from "@/modules/api-key/api-key.service";
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
providers: [ApiKeyService],
|
providers: [ApiKeyRepository],
|
||||||
exports: [ApiKeyService],
|
exports: [ApiKeyRepository],
|
||||||
})
|
})
|
||||||
export class ApiKeyModule {}
|
export class ApiKeyModule {}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { hashAPIKey } from "@/lib/api-key";
|
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import type { Request } from "express";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ApiKeyService {
|
|
||||||
constructor(private readonly dbRead: PrismaReadService) {}
|
|
||||||
|
|
||||||
async retrieveApiKey(request: Request) {
|
|
||||||
const apiKey = request.get("Authorization")?.replace("Bearer ", "");
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedKey = hashAPIKey(apiKey.replace("cal_", ""));
|
|
||||||
|
|
||||||
return this.dbRead.prisma.apiKey.findUniqueOrThrow({
|
|
||||||
where: {
|
|
||||||
hashedKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||||
import { AccessTokenStrategy } from "@/modules/auth/strategies/access-token/access-token.strategy";
|
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||||
import { ApiKeyAuthStrategy } from "@/modules/auth/strategies/api-key-auth/api-key-auth.strategy";
|
|
||||||
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
|
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
|
||||||
|
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { RedisModule } from "@/modules/redis/redis.module";
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
@@ -13,15 +13,16 @@ import { Module } from "@nestjs/common";
|
|||||||
import { PassportModule } from "@nestjs/passport";
|
import { PassportModule } from "@nestjs/passport";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PassportModule, RedisModule, ApiKeyModule, UsersModule, MembershipsModule, TokensModule],
|
imports: [
|
||||||
providers: [
|
PassportModule,
|
||||||
ApiKeyAuthStrategy,
|
RedisModule,
|
||||||
NextAuthGuard,
|
ApiKeyModule,
|
||||||
NextAuthStrategy,
|
UsersModule,
|
||||||
AccessTokenGuard,
|
MembershipsModule,
|
||||||
AccessTokenStrategy,
|
TokensModule,
|
||||||
OAuthFlowService,
|
DeploymentsModule,
|
||||||
],
|
],
|
||||||
exports: [NextAuthGuard, AccessTokenGuard],
|
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||||
|
exports: [NextAuthGuard, ApiAuthGuard],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
@@ -1,26 +1,33 @@
|
|||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { ExecutionContext } from "@nestjs/common";
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
import { createParamDecorator } from "@nestjs/common";
|
import { createParamDecorator } from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
|
||||||
|
|
||||||
export const GetUser = createParamDecorator<keyof User | (keyof User)[], ExecutionContext>((data, ctx) => {
|
export type GetUserReturnType = UserWithProfile & { isSystemAdmin: boolean };
|
||||||
|
|
||||||
|
export const GetUser = createParamDecorator<
|
||||||
|
keyof GetUserReturnType | (keyof GetUserReturnType)[],
|
||||||
|
ExecutionContext
|
||||||
|
>((data, ctx) => {
|
||||||
const request = ctx.switchToHttp().getRequest();
|
const request = ctx.switchToHttp().getRequest();
|
||||||
const user = request.user as User;
|
const user = request.user as GetUserReturnType;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("GetUser decorator : User not found");
|
throw new Error("GetUser decorator : User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.isSystemAdmin = user.role === "ADMIN";
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data.reduce((prev, curr) => {
|
return data.reduce((prev, curr) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[curr]: request.user[curr],
|
[curr]: user[curr],
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
return request.user[data];
|
return user[data];
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { AuthGuard } from "@nestjs/passport";
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
|
||||||
export class AccessTokenGuard extends AuthGuard("access-token") {
|
export class ApiAuthGuard extends AuthGuard("api-auth") {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -1,6 +1,7 @@
|
|||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { createMock } from "@golevelup/ts-jest";
|
import { createMock } from "@golevelup/ts-jest";
|
||||||
import { ExecutionContext } from "@nestjs/common";
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
import { APPS_WRITE, SCHEDULE_READ, SCHEDULE_WRITE } from "@calcom/platform-constants";
|
import { APPS_WRITE, SCHEDULE_READ, SCHEDULE_WRITE } from "@calcom/platform-constants";
|
||||||
@@ -13,7 +14,7 @@ describe("PermissionsGuard", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
reflector = new Reflector();
|
reflector = new Reflector();
|
||||||
guard = new PermissionsGuard(reflector, createMock<TokensRepository>());
|
guard = new PermissionsGuard(reflector, createMock<TokensRepository>(), createMock<ConfigService>());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be defined", () => {
|
it("should be defined", () => {
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { isApiKey } from "@/lib/api-key";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
import { hasPermissions } from "@calcom/platform-utils";
|
import { hasPermissions } from "@calcom/platform-utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PermissionsGuard implements CanActivate {
|
export class PermissionsGuard implements CanActivate {
|
||||||
constructor(private reflector: Reflector, private tokensRepository: TokensRepository) {}
|
constructor(
|
||||||
|
private reflector: Reflector,
|
||||||
|
private tokensRepository: TokensRepository,
|
||||||
|
private readonly config: ConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const requiredPermissions = this.reflector.get(Permissions, context.getHandler());
|
const requiredPermissions = this.reflector.get(Permissions, context.getHandler());
|
||||||
@@ -17,13 +23,18 @@ export class PermissionsGuard implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const accessToken = request.get("Authorization")?.replace("Bearer ", "");
|
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!authString) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oAuthClientPermissions = await this.getOAuthClientPermissions(accessToken);
|
// only check permissions for accessTokens attached to an oAuth Client
|
||||||
|
if (isApiKey(authString, this.config.get("api.apiKeyPrefix") ?? "cal_")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oAuthClientPermissions = await this.getOAuthClientPermissions(authString);
|
||||||
|
|
||||||
if (!oAuthClientPermissions) {
|
if (!oAuthClientPermissions) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
|
||||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import type { Request } from "express";
|
|
||||||
|
|
||||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AccessTokenStrategy extends PassportStrategy(BaseStrategy, "access-token") {
|
|
||||||
constructor(
|
|
||||||
private readonly oauthFlowService: OAuthFlowService,
|
|
||||||
private readonly tokensRepository: TokensRepository,
|
|
||||||
private readonly userRepository: UsersRepository
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async authenticate(request: Request) {
|
|
||||||
try {
|
|
||||||
const accessToken = request.get("Authorization")?.replace("Bearer ", "");
|
|
||||||
const requestOrigin = request.get("Origin");
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
|
||||||
if (!accessTokenValid) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await this.tokensRepository.getAccessTokenClient(accessToken);
|
|
||||||
if (!client) {
|
|
||||||
throw new UnauthorizedException("OAuth client not found given the access token");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestOrigin && !client.redirectUris.some((uri) => uri.startsWith(requestOrigin))) {
|
|
||||||
throw new UnauthorizedException(
|
|
||||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${requestOrigin}' to the 'Redirect uris' of your OAuth client.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
|
||||||
|
|
||||||
if (!ownerId) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.success(user);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) return this.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import appConfig from "@/config/app";
|
||||||
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
|
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||||
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
|
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||||
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { ExecutionContext, HttpException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
import { JwtService as NestJwtService } from "@nestjs/jwt";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { MockedRedisService } from "test/mocks/mock-redis-service";
|
||||||
|
|
||||||
|
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||||
|
|
||||||
|
describe("ApiAuthStrategy", () => {
|
||||||
|
let strategy: ApiAuthStrategy;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let organization: Team;
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
const validApiKeyEmail = "api-key-user-email@example.com";
|
||||||
|
const validAccessTokenEmail = "access-token-user-email@example.com";
|
||||||
|
let validApiKeyUser: User;
|
||||||
|
let validAccessTokenUser: User;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
ignoreEnvFile: true,
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfig],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
MockedRedisService,
|
||||||
|
ApiAuthStrategy,
|
||||||
|
ConfigService,
|
||||||
|
OAuthFlowService,
|
||||||
|
UsersRepository,
|
||||||
|
ApiKeyRepository,
|
||||||
|
DeploymentsService,
|
||||||
|
OAuthClientRepository,
|
||||||
|
PrismaReadService,
|
||||||
|
PrismaWriteService,
|
||||||
|
TokensRepository,
|
||||||
|
JwtService,
|
||||||
|
DeploymentsRepository,
|
||||||
|
NestJwtService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
strategy = module.get<ApiAuthStrategy>(ApiAuthStrategy);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(module);
|
||||||
|
tokensRepositoryFixture = new TokensRepositoryFixture(module);
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(module);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||||
|
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||||
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
|
validApiKeyUser = await userRepositoryFixture.create({
|
||||||
|
email: validApiKeyEmail,
|
||||||
|
});
|
||||||
|
validAccessTokenUser = await userRepositoryFixture.create({
|
||||||
|
email: validAccessTokenEmail,
|
||||||
|
});
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: ["http://localhost:3000"],
|
||||||
|
permissions: 32,
|
||||||
|
};
|
||||||
|
oAuthClient = await oAuthClientRepositoryFixture.create(organization.id, data, "secret");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authenticate with strategy", () => {
|
||||||
|
it("should return user associated with valid access token", async () => {
|
||||||
|
console.log("HERERERER");
|
||||||
|
const { accessToken } = await tokensRepositoryFixture.createTokens(
|
||||||
|
validAccessTokenUser.id,
|
||||||
|
oAuthClient.id
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Access token", accessToken);
|
||||||
|
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
get: (key: string) =>
|
||||||
|
({ Authorization: `Bearer ${accessToken}`, origin: "http://localhost:3000" }[key]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as ExecutionContext;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const user = await strategy.authenticate(request);
|
||||||
|
|
||||||
|
await expect(user.id).toEqual(validAccessTokenUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return user associated with valid api key", async () => {
|
||||||
|
const now = new Date();
|
||||||
|
now.setDate(now.getDate() + 1);
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(validApiKeyUser.id, now);
|
||||||
|
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer cal_test_${keyString}`,
|
||||||
|
},
|
||||||
|
get: (key: string) =>
|
||||||
|
({ Authorization: `Bearer cal_test_${keyString}`, origin: "http://localhost:3000" }[key]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as ExecutionContext;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const user = await strategy.authenticate(request);
|
||||||
|
|
||||||
|
expect(user.id).toEqual(validApiKeyUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw 401 if api key is invalid", async () => {
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer cal_test_}`,
|
||||||
|
},
|
||||||
|
get: (key: string) =>
|
||||||
|
({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" }[key]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as ExecutionContext;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await strategy.authenticate(request);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
expect(error.getStatus()).toEqual(401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw 401 if Authorization header does not contain auth token", async () => {
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
get: (key: string) => ({ Authorization: ``, origin: "http://localhost:3000" }[key]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as ExecutionContext;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await strategy.authenticate(request);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
expect(error.getStatus()).toEqual(401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loaded 100 of 663 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user