Compare commits
291
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b84405c5eb | ||
|
|
2743b32536 | ||
|
|
a251422f7a | ||
|
|
d7c9bd6ed8 | ||
|
|
8b3ff0b789 | ||
|
|
94a537bfda | ||
|
|
64305ffee1 | ||
|
|
45698db0b4 | ||
|
|
14e14289f0 | ||
|
|
18650b1215 | ||
|
|
d2bbbf3f45 | ||
|
|
13cb99b7fe | ||
|
|
a1571d01bd | ||
|
|
bbd8af074e | ||
|
|
c9a47dd0ce | ||
|
|
1c9778e667 | ||
|
|
758143ef70 | ||
|
|
b5911a9a2d | ||
|
|
260f4e1b51 | ||
|
|
e6c36af3b4 | ||
|
|
2fb2458eee | ||
|
|
b340b0edbe | ||
|
|
f7fd1c4402 | ||
|
|
c316721051 | ||
|
|
80ac118242 | ||
|
|
efd15c5a3e | ||
|
|
7d02d1bb23 | ||
|
|
0d877e7be3 | ||
|
|
e4c47640fc | ||
|
|
89e7fca183 | ||
|
|
33b5bf1db8 | ||
|
|
f69c4a900e | ||
|
|
1da25cc85f | ||
|
|
3a65b372c5 | ||
|
|
9892d521fb | ||
|
|
3285b252c2 | ||
|
|
97af2fc0f9 | ||
|
|
5c508bce6d | ||
|
|
db84343581 | ||
|
|
2b44d09191 | ||
|
|
10d67aee86 | ||
|
|
b33bbb8af0 | ||
|
|
3fe9327724 | ||
|
|
179a547bd0 | ||
|
|
7ada02177d | ||
|
|
59b55e40b5 | ||
|
|
19563aa697 | ||
|
|
6410687fb6 | ||
|
|
4e4c8f5f49 | ||
|
|
416b17cf69 | ||
|
|
27cd7c8496 | ||
|
|
02321c33b2 | ||
|
|
c3dab3e4f8 | ||
|
|
0c182e2813 | ||
|
|
51a076212b | ||
|
|
27dce7374b | ||
|
|
52f2c18800 | ||
|
|
35a8918ea9 | ||
|
|
b39eab72e7 | ||
|
|
928b2477be | ||
|
|
dc13251bcc | ||
|
|
4843658dfb | ||
|
|
076afdc00e | ||
|
|
4404699fa6 | ||
|
|
52940ce254 | ||
|
|
cd6293d220 | ||
|
|
108f95723b | ||
|
|
f611c3d767 | ||
|
|
6091208f8a | ||
|
|
585a12fa48 | ||
|
|
bfc4f7b66c | ||
|
|
9dca13305f | ||
|
|
0ab9823c9a | ||
|
|
b3fd4f307f | ||
|
|
06ed0de456 | ||
|
|
79b6278883 | ||
|
|
673823cb97 | ||
|
|
15b26dff73 | ||
|
|
2303f2e9e1 | ||
|
|
056b821070 | ||
|
|
c90fded6b7 | ||
|
|
aa6f1a7f93 | ||
|
|
0cdd7c9605 | ||
|
|
f40a1c92ef | ||
|
|
739ef153f6 | ||
|
|
850ac8092f | ||
|
|
5b1eb48e6b | ||
|
|
da6fc9f2d5 | ||
|
|
20434856e9 | ||
|
|
7f891adff2 | ||
|
|
eb66fbe054 | ||
|
|
29122934a9 | ||
|
|
0825cce82c | ||
|
|
fd964131d1 | ||
|
|
77d9c15fed | ||
|
|
b21295f268 | ||
|
|
5769553659 | ||
|
|
d23d68bf2d | ||
|
|
d04c7f58a1 | ||
|
|
93e2426760 | ||
|
|
9f6c05f389 | ||
|
|
f8a1bd7b94 | ||
|
|
02967961f3 | ||
|
|
053b8b5c0a | ||
|
|
d59efd3ba0 | ||
|
|
ce56cd1ab0 | ||
|
|
54b6deee19 | ||
|
|
8315641724 | ||
|
|
aa3086cfae | ||
|
|
b599ce4919 | ||
|
|
52f16604c1 | ||
|
|
c98c06062b | ||
|
|
4963d247dc | ||
|
|
9405bc9553 | ||
|
|
8a080b91b9 | ||
|
|
4aa05fe523 | ||
|
|
d74d99dde7 | ||
|
|
0ee137efbb | ||
|
|
90f55670dc | ||
|
|
9c53f392ca | ||
|
|
cc19a81875 | ||
|
|
67032ddaf1 | ||
|
|
2fe71c3695 | ||
|
|
21422fe027 | ||
|
|
95575291ff | ||
|
|
3e61a0633d | ||
|
|
85c9a790f2 | ||
|
|
e17d55457f | ||
|
|
d8d8bc4d00 | ||
|
|
55cc1aea59 | ||
|
|
ae383164bc | ||
|
|
0d8863fe8e | ||
|
|
51f6654635 | ||
|
|
fa11e10bcb | ||
|
|
a8f5bd6fe9 | ||
|
|
a1c725bb35 | ||
|
|
e4eeb11a61 | ||
|
|
3b0bb25251 | ||
|
|
d27490e6f2 | ||
|
|
b3c191b553 | ||
|
|
0b977e615d | ||
|
|
979e1fd28b | ||
|
|
3437c4331d | ||
|
|
616d62094c | ||
|
|
ce7ff1c43c | ||
|
|
e27e41bc16 | ||
|
|
93e183e6c3 | ||
|
|
88a2bd3317 | ||
|
|
45caaa55b0 | ||
|
|
8c9b7a701d | ||
|
|
c47b992125 | ||
|
|
493c4f89dc | ||
|
|
27234dc809 | ||
|
|
5690b8e1d5 | ||
|
|
61274bc7ef | ||
|
|
e66be23996 | ||
|
|
3c43ce4165 | ||
|
|
5060824795 | ||
|
|
c4941f25a2 | ||
|
|
e7c219905f | ||
|
|
499cbd08ca | ||
|
|
0a3b3a69e3 | ||
|
|
e8607845e2 | ||
|
|
02ef4db0eb | ||
|
|
f8d48a341d | ||
|
|
251393101a | ||
|
|
b50430114f | ||
|
|
b0999abc68 | ||
|
|
2bbd92db13 | ||
|
|
64092abdca | ||
|
|
b7b631f0b0 | ||
|
|
c10f15e75b | ||
|
|
6983179b2f | ||
|
|
aea0661303 | ||
|
|
3fd9ef366f | ||
|
|
cf79a3f580 | ||
|
|
c5da78593a | ||
|
|
801b0be414 | ||
|
|
29aad2c535 | ||
|
|
82d3b9d29d | ||
|
|
a542ba486c | ||
|
|
d50e1a0e15 | ||
|
|
79a0071d65 | ||
|
|
2f1f989266 | ||
|
|
2303eb7908 | ||
|
|
c3666dd064 | ||
|
|
6ed197d35c | ||
|
|
164c9e37d7 | ||
|
|
fa0a18b31d | ||
|
|
80e2118e68 | ||
|
|
ed5086df6b | ||
|
|
218ec9140c | ||
|
|
6dad0e7679 | ||
|
|
cb96a56960 | ||
|
|
b35eb77edb | ||
|
|
89408ce12b | ||
|
|
e22d9a551c | ||
|
|
c215dc5864 | ||
|
|
a37f1d9798 | ||
|
|
07c8bbc562 | ||
|
|
b381cfe207 | ||
|
|
a560b9c8f4 | ||
|
|
e5b8cc6912 | ||
|
|
18b5612c0d | ||
|
|
fad488623f | ||
|
|
c490f084dc | ||
|
|
b1caacd643 | ||
|
|
76c8876923 | ||
|
|
a911c9cb8d | ||
|
|
07d5594085 | ||
|
|
a745d8a60a | ||
|
|
8b83d39ac3 | ||
|
|
4edb39616d | ||
|
|
7a2060fe08 | ||
|
|
d009682d4a | ||
|
|
8adb720a80 | ||
|
|
4d6f338f0c | ||
|
|
ffe9d43d09 | ||
|
|
2227b4ff91 | ||
|
|
5dd4937a1a | ||
|
|
866a0e2596 | ||
|
|
64bfda9dbe | ||
|
|
aacbbc1785 | ||
|
|
3714d5faab | ||
|
|
12cb94950a | ||
|
|
795fd788e2 | ||
|
|
956fd6ae2f | ||
|
|
04e60611ed | ||
|
|
bc99e9e504 | ||
|
|
2f35428232 | ||
|
|
00aac89ed7 | ||
|
|
f390e91bd7 | ||
|
|
e413359c8c | ||
|
|
601df7f808 | ||
|
|
e160a4add0 | ||
|
|
3bea03f4b1 | ||
|
|
19dbde8a3c | ||
|
|
a5fe10189f | ||
|
|
7075384313 | ||
|
|
25bf43cddd | ||
|
|
2880a681a8 | ||
|
|
bbcd0b7e78 | ||
|
|
916ed13bea | ||
|
|
11a06324be | ||
|
|
bcb340d97e | ||
|
|
aabef47ff1 | ||
|
|
e27de47d9f | ||
|
|
7900be3a30 | ||
|
|
ddc5838c7b | ||
|
|
6a13d7c5c6 | ||
|
|
4f0b35d9e2 | ||
|
|
3b19ede6a3 | ||
|
|
1a0ce47404 | ||
|
|
22f136d19b | ||
|
|
9a40e54159 | ||
|
|
229f2627c8 | ||
|
|
6f6454df40 | ||
|
|
3da9fc5500 | ||
|
|
540bf868b5 | ||
|
|
1674ecc585 | ||
|
|
4920abdaf7 | ||
|
|
a6b98bd35d | ||
|
|
34d4f394fc | ||
|
|
a4ef8d4200 | ||
|
|
9d9662687e | ||
|
|
2d41c719c4 | ||
|
|
b69bd966a6 | ||
|
|
09a49dd0fd | ||
|
|
3dd3372c35 | ||
|
|
0a60864310 | ||
|
|
cb24e5d0aa | ||
|
|
8c5aebdca1 | ||
|
|
4e2cff0fd5 | ||
|
|
54e518efad | ||
|
|
ad93099171 | ||
|
|
630065ca89 | ||
|
|
ddf5717992 | ||
|
|
4a53ec0b74 | ||
|
|
d7fd055171 | ||
|
|
874973d78f | ||
|
|
8078c5657b | ||
|
|
b39404860f | ||
|
|
860eae8dcb | ||
|
|
b931d3ee13 | ||
|
|
ba8a71bf3a | ||
|
|
8de4897042 | ||
|
|
6de6f8e697 | ||
|
|
e55ecff899 | ||
|
|
3895ef002e | ||
|
|
afc28503a5 | ||
|
|
e6497c87ca |
@@ -7,7 +7,7 @@
|
|||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": [],
|
"ignore": ["@calcom/platform-libraries"],
|
||||||
"privatePackages": {
|
"privatePackages": {
|
||||||
"version": false,
|
"version": false,
|
||||||
"tag": false
|
"tag": false
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ SENTRY_PROJECT=
|
|||||||
SENTRY_AUTH_TOKEN=
|
SENTRY_AUTH_TOKEN=
|
||||||
# Used for capturing exceptions and logging messages
|
# Used for capturing exceptions and logging messages
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN_CLIENT=
|
||||||
SENTRY_DEBUG=
|
SENTRY_DEBUG=
|
||||||
SENTRY_MAX_SPANS=
|
SENTRY_MAX_SPANS=
|
||||||
SENTRY_SAMPLE_RATE=
|
SENTRY_SAMPLE_RATE=
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
||||||
/packages/lib/slots.ts @calcom/Foundation
|
/packages/lib/slots.ts @calcom/Foundation
|
||||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||||
/packages/prisma/**/* @calcom/Foundation
|
/packages/prisma/**/* @calcom/DBA
|
||||||
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
contents: write # to create release by changesets/action below
|
contents: write # to create release by changesets/action below
|
||||||
pull-requests: write # to create pull request by changesets/action below
|
pull-requests: write # to create pull request by changesets/action below
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -27,8 +27,24 @@ jobs:
|
|||||||
|
|
||||||
- name: Configure git
|
- name: Configure git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "github-actions@github.com"
|
# Define authorized users and their emails using organization secret
|
||||||
git config --local user.name "GitHub Actions"
|
# Expected format: JSON object with username -> email mapping
|
||||||
|
# Example: {"zomars": "zomars@cal.com", "peer": "peer@cal.com", "username3": "email3@cal.com"}
|
||||||
|
USER_EMAILS='${{ secrets.RELEASE_USER_EMAILS }}'
|
||||||
|
|
||||||
|
# Extract email for the triggering user
|
||||||
|
USER_EMAIL=$(echo "$USER_EMAILS" | jq -r --arg user "${{ github.actor }}" '.[$user] // empty')
|
||||||
|
|
||||||
|
# Fail if user is not authorized or not in mapping
|
||||||
|
if [ -z "$USER_EMAIL" ] || [ "$USER_EMAIL" = "null" ]; then
|
||||||
|
echo "Error: User '${{ github.actor }}' is not authorized to run the release workflow."
|
||||||
|
echo "Only authorized team members can trigger releases."
|
||||||
|
echo "Contact your administrator to be added to the authorized users list."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --local user.email "$USER_EMAIL"
|
||||||
|
git config --local user.name "${{ github.actor }}"
|
||||||
|
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
- uses: lingodotdev/lingo.dev@main
|
- uses: lingodotdev/lingo.dev@main
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
organization-name: calcom
|
organization-name: calcom
|
||||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||||
apply-labels-from-issue:
|
apply-labels-from-issue:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
outputs:
|
outputs:
|
||||||
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || (github.event.action == 'labeled' && github.event.label.name == 'ready-for-e2e')) }}
|
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || github.event.label.name == 'ready-for-e2e') }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check if PR exists with ready-for-e2e label for this SHA
|
- name: Check if PR exists with ready-for-e2e label for this SHA
|
||||||
id: check-if-pr-has-label
|
id: check-if-pr-has-label
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -488,13 +488,8 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
|
|||||||
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||||
|
|
||||||
## Single Org Mode
|
## Single Org Mode
|
||||||
If you want to have booker.yourcompany.com to be the domain used for both dashboard(e.g. https://booker.yourcompany.com/event-types) and booking pages(e.g. https://booker.yourcompany.com/john.joe/15min).
|
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
|
||||||
- Set the `NEXT_PUBLIC_SINGLE_ORG_SLUG` environment variable to the slug of the organization you want to use. `NEXT_PUBLIC_SINGLE_ORG_SLUG=booker`
|
|
||||||
- Set the `NEXT_PUBLIC_WEBAPP_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBAPP_URL=https://booker.yourcompany.com`.
|
|
||||||
- Set the `NEXT_PUBLIC_WEBSITE_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBSITE_URL=https://booker.yourcompany.com`.
|
|
||||||
- Set the `NEXTAUTH_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXTAUTH_URL=https://booker.yourcompany.com`.
|
|
||||||
|
|
||||||
Note: It causes root to serve the dashboard and not the organization profile page which shows all bookable users in the organization.
|
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node ./index.js"
|
"dev": "node ./index.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
const mockDeploymentRepository: IDeploymentRepository = {
|
const mockDeploymentRepository: IDeploymentRepository = {
|
||||||
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
||||||
|
getSignatureToken: vi.fn().mockResolvedValue("mockSignatureToken"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Fix the skip condition for this test suite
|
// TODO: Fix the skip condition for this test suite
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum locales {
|
|||||||
SR = "sr",
|
SR = "sr",
|
||||||
SV = "sv",
|
SV = "sv",
|
||||||
VI = "vi",
|
VI = "vi",
|
||||||
|
BN = "bn",
|
||||||
}
|
}
|
||||||
enum theme {
|
enum theme {
|
||||||
DARK = "dark",
|
DARK = "dark",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ const plugins = [withAxiom];
|
|||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
experimental: {
|
|
||||||
instrumentationHook: true,
|
|
||||||
},
|
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@calcom/app-store",
|
"@calcom/app-store",
|
||||||
"@calcom/dayjs",
|
"@calcom/dayjs",
|
||||||
|
|||||||
@@ -33,12 +33,12 @@
|
|||||||
"@sentry/nextjs": "^9.15.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.5.9",
|
"next": "^15.3.0",
|
||||||
"next-api-middleware": "^1.0.1",
|
"next-api-middleware": "^1.0.1",
|
||||||
"next-axiom": "^0.17.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-swagger-doc": "^0.3.6",
|
"next-swagger-doc": "^0.3.6",
|
||||||
"next-validations": "^0.2.0",
|
"next-validations": "^0.2.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.7.2",
|
||||||
"tzdata": "^1.0.30",
|
"tzdata": "^1.0.30",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
|
|
||||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||||
|
|
||||||
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
|
const usersWithCalendars = await new UserRepository(
|
||||||
|
prisma
|
||||||
|
).findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||||
ids: userIds,
|
ids: userIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ describe("GET /api/slots", () => {
|
|||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|
||||||
expect(res.statusCode).toBe(400);
|
expect(res.statusCode).toBe(400);
|
||||||
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"message": "invalid_type in 'startTime': Required; invalid_type in 'endTime': Required",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||||
|
import { getAvailableSlotsService } from "@calcom/lib/di/containers/available-slots";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types";
|
import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
|
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||||
@@ -27,7 +27,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
|
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
|
||||||
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
||||||
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
|
||||||
|
const availableSlotsService = getAvailableSlotsService();
|
||||||
|
const availableSlots = await availableSlotsService.getAvailableSlots({
|
||||||
|
ctx: await createContext({ req, res }),
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
|
||||||
const slotsInProvidedTimeZone = timeZoneSupported
|
const slotsInProvidedTimeZone = timeZoneSupported
|
||||||
? Object.keys(availableSlots.slots).reduce(
|
? Object.keys(availableSlots.slots).reduce(
|
||||||
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
watchAttempts: 0,
|
watchAttempts: 0,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
@@ -136,6 +138,8 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
watchAttempts: 0,
|
watchAttempts: 0,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"description": "Platform API for Cal.com",
|
"description": "Platform API for Cal.com",
|
||||||
"author": "Cal.com Inc.",
|
"author": "Cal.com Inc.",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"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\"",
|
||||||
@@ -37,8 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.214",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.257",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.4.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "^4.1.0",
|
"tsconfig-paths": "^4.1.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"schema": "../../../packages/prisma/schema.prisma"
|
"schema": "../../../packages/prisma/schema.prisma"
|
||||||
|
|||||||
@@ -86,10 +86,16 @@ export class AppModule implements NestModule {
|
|||||||
configure(consumer: MiddlewareConsumer): void {
|
configure(consumer: MiddlewareConsumer): void {
|
||||||
consumer
|
consumer
|
||||||
.apply(RawBodyMiddleware)
|
.apply(RawBodyMiddleware)
|
||||||
.forRoutes({
|
.forRoutes(
|
||||||
path: "/api/v2/billing/webhook",
|
{
|
||||||
method: RequestMethod.POST,
|
path: "/api/v2/billing/webhook",
|
||||||
})
|
method: RequestMethod.POST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/v2/billing/webhook",
|
||||||
|
method: RequestMethod.POST,
|
||||||
|
}
|
||||||
|
)
|
||||||
.apply(JsonBodyMiddleware)
|
.apply(JsonBodyMiddleware)
|
||||||
.forRoutes("*")
|
.forRoutes("*")
|
||||||
.apply(RequestIdMiddleware)
|
.apply(RequestIdMiddleware)
|
||||||
|
|||||||
@@ -81,5 +81,9 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
if (process?.env?.API_GLOBAL_PREFIX) {
|
||||||
|
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type OAuthRequestParams = {
|
|||||||
platformBookingUrl: string;
|
platformBookingUrl: string;
|
||||||
platformBookingLocation?: string;
|
platformBookingLocation?: string;
|
||||||
arePlatformEmailsEnabled: boolean;
|
arePlatformEmailsEnabled: boolean;
|
||||||
|
areCalendarEventsEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PLATFORM_PARAMS = {
|
const DEFAULT_PLATFORM_PARAMS = {
|
||||||
@@ -86,6 +87,7 @@ const DEFAULT_PLATFORM_PARAMS = {
|
|||||||
platformBookingUrl: "",
|
platformBookingUrl: "",
|
||||||
arePlatformEmailsEnabled: false,
|
arePlatformEmailsEnabled: false,
|
||||||
platformBookingLocation: undefined,
|
platformBookingLocation: undefined,
|
||||||
|
areCalendarEventsEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -195,6 +197,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
if (booking.userId && booking.uid && booking.startTime) {
|
if (booking.userId && booking.uid && booking.startTime) {
|
||||||
void (await this.billingService.increaseUsageByUserId(booking.userId, {
|
void (await this.billingService.increaseUsageByUserId(booking.userId, {
|
||||||
@@ -229,6 +232,15 @@ export class BookingsController_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bookingUid) {
|
if (bookingUid) {
|
||||||
|
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||||
|
if (!bookingInfo) {
|
||||||
|
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||||
|
}
|
||||||
|
if (bookingInfo.status === "CANCELLED") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't cancel booking with uid=${bookingUid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
req.body.uid = bookingUid;
|
req.body.uid = bookingUid;
|
||||||
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||||
@@ -428,7 +440,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
|
|
||||||
if (isEmbed) {
|
if (isEmbed) {
|
||||||
// embed should ignore oauth client settings and enable emails by default
|
// embed should ignore oauth client settings and enable emails by default
|
||||||
return { ...res, arePlatformEmailsEnabled: true };
|
return { ...res, arePlatformEmailsEnabled: true, areCalendarEventsEnabled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -440,6 +452,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||||
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||||
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
||||||
|
res.areCalendarEventsEnabled = client.areCalendarEventsEnabled;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -24,4 +24,26 @@ export class BookingReferencesRepository_2024_08_13 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBookingReferencesIncludeSensitiveCredentials(eventUid: string) {
|
||||||
|
return this.dbRead.prisma.bookingReference.findFirst({
|
||||||
|
where: {
|
||||||
|
uid: eventUid,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
credential: true,
|
||||||
|
delegationCredential: true,
|
||||||
|
booking: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
|
|||||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
|
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||||
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
||||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||||
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
@@ -77,6 +78,7 @@ import { Module } from "@nestjs/common";
|
|||||||
ErrorsBookingsService_2024_08_13,
|
ErrorsBookingsService_2024_08_13,
|
||||||
BookingReferencesService_2024_08_13,
|
BookingReferencesService_2024_08_13,
|
||||||
BookingReferencesRepository_2024_08_13,
|
BookingReferencesRepository_2024_08_13,
|
||||||
|
CalVideoService,
|
||||||
],
|
],
|
||||||
controllers: [BookingsController_2024_08_13],
|
controllers: [BookingsController_2024_08_13],
|
||||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||||
|
|||||||
@@ -169,4 +169,15 @@ export class BookingsRepository_2024_08_13 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByUidWithBookingReference(uid: string) {
|
||||||
|
return this.dbRead.prisma.booking.findUnique({
|
||||||
|
where: {
|
||||||
|
uid,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
references: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outpu
|
|||||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
|
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
@@ -54,6 +55,8 @@ import {
|
|||||||
RescheduleBookingInput_2024_08_13,
|
RescheduleBookingInput_2024_08_13,
|
||||||
RescheduleBookingInputPipe,
|
RescheduleBookingInputPipe,
|
||||||
RescheduleSeatedBookingInput_2024_08_13,
|
RescheduleSeatedBookingInput_2024_08_13,
|
||||||
|
GetBookingRecordingsOutput,
|
||||||
|
GetBookingTranscriptsOutput,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import {
|
import {
|
||||||
CreateBookingInputPipe,
|
CreateBookingInputPipe,
|
||||||
@@ -88,7 +91,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly bookingsService: BookingsService_2024_08_13,
|
private readonly bookingsService: BookingsService_2024_08_13,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly bookingReferencesService: BookingReferencesService_2024_08_13
|
private readonly bookingReferencesService: BookingReferencesService_2024_08_13,
|
||||||
|
private readonly calVideoService: CalVideoService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -171,6 +175,36 @@ export class BookingsController_2024_08_13 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/recordings")
|
||||||
|
@UseGuards(BookingUidGuard)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get all the recordings for the booking",
|
||||||
|
description: `Fetches all the recordings for the booking \`:bookingUid\``,
|
||||||
|
})
|
||||||
|
async getBookingRecordings(@Param("bookingUid") bookingUid: string): Promise<GetBookingRecordingsOutput> {
|
||||||
|
const recordings = await this.calVideoService.getRecordings(bookingUid);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: recordings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/transcripts")
|
||||||
|
@UseGuards(BookingUidGuard)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get all the transcripts download links for the booking",
|
||||||
|
description: `Fetches all the transcripts download links for the booking \`:bookingUid\``,
|
||||||
|
})
|
||||||
|
async getBookingTranscripts(@Param("bookingUid") bookingUid: string): Promise<GetBookingTranscriptsOutput> {
|
||||||
|
const transcripts = await this.calVideoService.getTranscripts(bookingUid);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: transcripts ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -233,7 +267,14 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Cancel a booking",
|
summary: "Cancel a booking",
|
||||||
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
|
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
|
||||||
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
|
|
||||||
|
\nCancelling seated bookings:
|
||||||
|
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
|
||||||
|
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
|
||||||
|
\n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
|
||||||
|
|
||||||
|
\nCancelling recurring seated bookings:
|
||||||
|
For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
@@ -402,7 +443,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
@Permissions([BOOKING_READ])
|
@Permissions([BOOKING_READ])
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get 'Booking References' for a booking",
|
summary: "Get booking references",
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getBookingReferences(
|
async getBookingReferences(
|
||||||
|
|||||||
+206
-78
@@ -14,29 +14,27 @@ import { Test } from "@nestjs/testing";
|
|||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
||||||
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 { 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 { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||||
import {
|
import {
|
||||||
|
CancelBookingInput_2024_08_13,
|
||||||
CancelSeatedBookingInput_2024_08_13,
|
CancelSeatedBookingInput_2024_08_13,
|
||||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||||
CreateSeatedBookingOutput_2024_08_13,
|
CreateSeatedBookingOutput_2024_08_13,
|
||||||
GetBookingOutput_2024_08_13,
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
|
||||||
GetSeatedBookingOutput_2024_08_13,
|
GetSeatedBookingOutput_2024_08_13,
|
||||||
RescheduleSeatedBookingInput_2024_08_13,
|
RescheduleSeatedBookingInput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import {
|
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||||
CreateBookingInput_2024_08_13,
|
|
||||||
CreateRecurringBookingInput_2024_08_13,
|
|
||||||
} from "@calcom/platform-types";
|
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
@@ -51,11 +49,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let schedulesService: SchedulesService_2024_04_15;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let oAuthClient: PlatformOAuthClient;
|
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
||||||
|
|
||||||
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
|
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
|
||||||
let user: User;
|
let user: User;
|
||||||
|
let apiKeyString: string;
|
||||||
|
|
||||||
let seatedEventTypeId: number;
|
let seatedEventTypeId: number;
|
||||||
const maxRecurrenceCount = 3;
|
const maxRecurrenceCount = 3;
|
||||||
@@ -63,6 +63,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||||
|
|
||||||
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
||||||
|
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
|
||||||
|
|
||||||
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
|
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
|
||||||
const nameAttendeeOne = `Attendee One ${randomString()}`;
|
const nameAttendeeOne = `Attendee One ${randomString()}`;
|
||||||
@@ -70,12 +71,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
|
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await Test.createTestingModule({
|
||||||
userEmail,
|
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||||
Test.createTestingModule({
|
})
|
||||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
.useValue({
|
.useValue({
|
||||||
canActivate: () => true,
|
canActivate: () => true,
|
||||||
@@ -87,22 +85,21 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
bookingSeatRepositoryFixture = new BookingSeatRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
organization = await teamRepositoryFixture.create({
|
organization = await teamRepositoryFixture.create({
|
||||||
name: `seated-bookings-organization-${randomString()}`,
|
name: `seated-bookings-organization-${randomString()}`,
|
||||||
});
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
platformOAuthClients: {
|
|
||||||
connect: {
|
|
||||||
id: oAuthClient.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||||
|
apiKeyString = `cal_test_${keyString}`;
|
||||||
|
|
||||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
|
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
|
||||||
timeZone: "Europe/Rome",
|
timeZone: "Europe/Rome",
|
||||||
@@ -118,6 +115,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
seatsShowAttendees: true,
|
seatsShowAttendees: true,
|
||||||
seatsShowAvailabilityCount: true,
|
seatsShowAvailabilityCount: true,
|
||||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||||
|
metadata: {
|
||||||
|
disableStandardEmails: {
|
||||||
|
all: {
|
||||||
|
attendee: true,
|
||||||
|
host: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
@@ -129,19 +134,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createOAuthClient(organizationId: number) {
|
|
||||||
const data = {
|
|
||||||
logo: "logo-url",
|
|
||||||
name: "name",
|
|
||||||
redirectUris: ["http://localhost:5555"],
|
|
||||||
permissions: 32,
|
|
||||||
};
|
|
||||||
const secret = "secret";
|
|
||||||
|
|
||||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should book an event type with seats for the first time", async () => {
|
it("should book an event type with seats for the first time", async () => {
|
||||||
const body: CreateBookingInput_2024_08_13 = {
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||||
@@ -333,6 +325,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get("/v2/bookings?sortCreated=asc")
|
.get("/v2/bookings?sortCreated=asc")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
@@ -350,6 +343,22 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not be able to reschedule seated booking if seatUid is not provided", async () => {
|
||||||
|
const body = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${createdSeatedBooking.uid}/reschedule`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Booking with uid=${createdSeatedBooking.uid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should reschedule seated booking", async () => {
|
it("should reschedule seated booking", async () => {
|
||||||
const body: RescheduleSeatedBookingInput_2024_08_13 = {
|
const body: RescheduleSeatedBookingInput_2024_08_13 = {
|
||||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||||
@@ -409,67 +418,186 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cancel seated booking", async () => {
|
describe("cancel seated booking", () => {
|
||||||
const body: CancelSeatedBookingInput_2024_08_13 = {
|
describe("cancel seated booking as attendee", () => {
|
||||||
seatUid: createdSeatedBooking.seatUid,
|
it("should cancel seated booking", async () => {
|
||||||
};
|
const body: CancelSeatedBookingInput_2024_08_13 = {
|
||||||
|
seatUid: createdSeatedBooking.seatUid,
|
||||||
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
|
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||||
expect(data.id).toBeDefined();
|
expect(data.id).toBeDefined();
|
||||||
expect(data.uid).toBeDefined();
|
expect(data.uid).toBeDefined();
|
||||||
expect(data.hosts[0].id).toEqual(user.id);
|
expect(data.hosts[0].id).toEqual(user.id);
|
||||||
expect(data.status).toEqual("cancelled");
|
expect(data.status).toEqual("cancelled");
|
||||||
expect(data.start).toEqual(createdSeatedBooking.start);
|
expect(data.start).toEqual(createdSeatedBooking.start);
|
||||||
expect(data.end).toEqual(createdSeatedBooking.end);
|
expect(data.end).toEqual(createdSeatedBooking.end);
|
||||||
expect(data.duration).toEqual(60);
|
expect(data.duration).toEqual(60);
|
||||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||||
expect(data.eventType).toEqual({
|
expect(data.eventType).toEqual({
|
||||||
id: seatedEventTypeId,
|
id: seatedEventTypeId,
|
||||||
slug: seatedEventSlug,
|
slug: seatedEventSlug,
|
||||||
|
});
|
||||||
|
expect(data.attendees.length).toEqual(0);
|
||||||
|
expect(data.location).toBeDefined();
|
||||||
|
expect(data.absentHost).toEqual(false);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid response data - expected booking but received array response");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expect(data.attendees.length).toEqual(0);
|
|
||||||
expect(data.location).toBeDefined();
|
|
||||||
expect(data.absentHost).toEqual(false);
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid response data - expected booking but received array response");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cancel seated booking as host", () => {
|
||||||
|
it("should book an event type with seats", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: seatedEventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: nameAttendeeOne,
|
||||||
|
email: emailAttendeeOne,
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
bookingFieldsResponses: {
|
||||||
|
codingLanguage: "TypeScript",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
userId: "100",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||||
|
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.seatUid).toBeDefined();
|
||||||
|
const seatUid = data.seatUid;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(data.hosts[0].id).toEqual(user.id);
|
||||||
|
expect(data.status).toEqual("accepted");
|
||||||
|
expect(data.start).toEqual(body.start);
|
||||||
|
expect(data.end).toEqual(
|
||||||
|
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||||
|
);
|
||||||
|
expect(data.duration).toEqual(60);
|
||||||
|
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||||
|
expect(data.eventType).toEqual({
|
||||||
|
id: seatedEventTypeId,
|
||||||
|
slug: seatedEventSlug,
|
||||||
|
});
|
||||||
|
expect(data.attendees.length).toEqual(1);
|
||||||
|
expect(data.attendees[0]).toEqual({
|
||||||
|
name: body.attendee.name,
|
||||||
|
email: body.attendee.email,
|
||||||
|
timeZone: body.attendee.timeZone,
|
||||||
|
language: body.attendee.language,
|
||||||
|
absent: false,
|
||||||
|
seatUid,
|
||||||
|
bookingFieldsResponses: {
|
||||||
|
name: body.attendee.name,
|
||||||
|
...body.bookingFieldsResponses,
|
||||||
|
},
|
||||||
|
metadata: body.metadata,
|
||||||
|
});
|
||||||
|
expect(data.location).toBeDefined();
|
||||||
|
expect(data.absentHost).toEqual(false);
|
||||||
|
createdSeatedBooking2 = data;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected recurring booking but received non array response"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to cancel without cancellation reason", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.message).toEqual("Cancellation reason is required when you are the host");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cancel seated booking", async () => {
|
||||||
|
const body: CancelBookingInput_2024_08_13 = {
|
||||||
|
cancellationReason: "I will be travelling without internet",
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||||
|
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(data.hosts[0].id).toEqual(user.id);
|
||||||
|
expect(data.status).toEqual("cancelled");
|
||||||
|
expect(data.start).toEqual(createdSeatedBooking2.start);
|
||||||
|
expect(data.end).toEqual(createdSeatedBooking2.end);
|
||||||
|
expect(data.duration).toEqual(60);
|
||||||
|
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||||
|
expect(data.eventType).toEqual({
|
||||||
|
id: seatedEventTypeId,
|
||||||
|
slug: seatedEventSlug,
|
||||||
|
});
|
||||||
|
expect(data.attendees.length).toEqual(0);
|
||||||
|
expect(data.location).toBeDefined();
|
||||||
|
expect(data.absentHost).toEqual(false);
|
||||||
|
expect(data.cancellationReason).toEqual("I will be travelling without internet");
|
||||||
|
|
||||||
|
const seats = await bookingSeatRepositoryFixture.findAllByBookingId(data.id);
|
||||||
|
expect(seats.length).toEqual(0);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid response data - expected booking but received array response");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
|
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
|
||||||
return data.hasOwnProperty("seatUid");
|
return data.hasOwnProperty("seatUid");
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseDataIsCreateRecurringSeatedBooking(
|
|
||||||
data: any
|
|
||||||
): data is CreateRecurringSeatedBookingOutput_2024_08_13[] {
|
|
||||||
return Array.isArray(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
|
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
|
||||||
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
|
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseDataIsGetRecurringSeatedBooking(
|
|
||||||
data: any
|
|
||||||
): data is GetRecurringSeatedBookingOutput_2024_08_13[] {
|
|
||||||
return Array.isArray(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
X_CAL_CLIENT_ID,
|
X_CAL_CLIENT_ID,
|
||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { AttendeeScheduledEmail, OrganizerScheduledEmail } from "@calcom/platform-libraries/emails";
|
import { AttendeeScheduledEmail, OrganizerScheduledEmail } from "@calcom/platform-libraries/emails";
|
||||||
|
import { EventManager } from "@calcom/platform-libraries/event-types";
|
||||||
import {
|
import {
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE,
|
FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE,
|
||||||
@@ -78,6 +79,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
||||||
let recurringEventTypeId: number;
|
let recurringEventTypeId: number;
|
||||||
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
||||||
|
let eventTypeRequiresConfirmationId: number;
|
||||||
|
const eventTypeRequiresConfirmationSlug = `user-bookings-event-type-requires-confirmation-${randomString()}`;
|
||||||
|
|
||||||
let createdBooking: BookingOutput_2024_08_13;
|
let createdBooking: BookingOutput_2024_08_13;
|
||||||
let rescheduledBooking: BookingOutput_2024_08_13;
|
let rescheduledBooking: BookingOutput_2024_08_13;
|
||||||
@@ -153,6 +156,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
);
|
);
|
||||||
recurringEventTypeId = recurringEvent.id;
|
recurringEventTypeId = recurringEvent.id;
|
||||||
|
|
||||||
|
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `user-bookings-2024-08-13-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: eventTypeRequiresConfirmationSlug,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||||
|
|
||||||
bookingInThePast = await bookingsRepositoryFixture.create({
|
bookingInThePast = await bookingsRepositoryFixture.create({
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
@@ -1411,8 +1425,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
expect(data.status).toEqual("cancelled");
|
expect(data.status).toEqual("cancelled");
|
||||||
|
expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid);
|
||||||
createdBooking = data;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2539,6 +2552,338 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("cant't cancel already cancelled booking", () => {
|
||||||
|
it("should not be able to cancel alraedy cancelled booking", async () => {
|
||||||
|
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "CANCELLED",
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `cancelled-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: CancelBookingInput_2024_08_13 = {
|
||||||
|
cancellationReason: "Going on a vacation",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${cancelledBooking.uid}/cancel`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Can't cancel booking with uid=${cancelledBooking.uid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("calendar events", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
jest
|
||||||
|
.spyOn(EventManager.prototype, "create")
|
||||||
|
.mockImplementation(() => Promise.resolve({ results: [], referencesToCreate: [] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("platform oAuth client has calendar events enabled", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.update(oAuthClient.id, {
|
||||||
|
areCalendarEventsEnabled: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create calendar event when booking normal event", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 9, 0, 0)).toISOString(),
|
||||||
|
eventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(1);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create calendar events when booking recurring event", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 10, 9, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: recurringEventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("event type requires confirmation", () => {
|
||||||
|
let bookingThatRequiresConfirmationUid: string;
|
||||||
|
|
||||||
|
it("should not create calendar event when booking event that requires confirmation", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 12, 9, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: eventTypeRequiresConfirmationId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
bookingThatRequiresConfirmationUid = data.uid;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create calendar event when confirming event that requires confirmation", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingThatRequiresConfirmationUid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(1);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("platform oAuth client has calendar events disabled", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.update(oAuthClient.id, {
|
||||||
|
areCalendarEventsEnabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create calendar event when booking", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeCreate = new Date();
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const afterCreate = new Date();
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create calendar events when booking recurring event", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 11, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: recurringEventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("event type requires confirmation", () => {
|
||||||
|
let bookingThatRequiresConfirmationUid: string;
|
||||||
|
|
||||||
|
it("should not create calendar event when booking event that requires confirmation", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 12, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: eventTypeRequiresConfirmationId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
bookingThatRequiresConfirmationUid = data.uid;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create calendar event when confirming event that requires confirmation", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingThatRequiresConfirmationUid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||||
return (
|
return (
|
||||||
!Array.isArray(data) &&
|
!Array.isArray(data) &&
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
noEmail: bookingRequest.noEmail,
|
noEmail: bookingRequest.noEmail,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
const ids = bookings.map((booking) => booking.id || 0);
|
const ids = bookings.map((booking) => booking.id || 0);
|
||||||
return this.outputService.getOutputRecurringBookings(ids);
|
return this.outputService.getOutputRecurringBookings(ids);
|
||||||
@@ -424,6 +425,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
|
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
|
||||||
@@ -445,6 +447,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
@@ -475,6 +478,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
@@ -592,9 +596,9 @@ export class BookingsService_2024_08_13 {
|
|||||||
} else if (isRecurring && isSeated) {
|
} else if (isRecurring && isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
||||||
} else if (isSeated) {
|
} else if (isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputSeatedBooking(formatted));
|
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
|
||||||
} else {
|
} else {
|
||||||
formattedBookings.push(this.outputService.getOutputBooking(formatted));
|
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,6 +656,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
throw new Error("Booking missing uid");
|
throw new Error("Booking missing uid");
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
|
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||||
|
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRecordingsOfCalVideoByRoomName,
|
||||||
|
getAllTranscriptsAccessLinkFromRoomName,
|
||||||
|
getDownloadLinkOfCalVideoByRecordingId,
|
||||||
|
} from "@calcom/platform-libraries/conferencing";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalVideoService {
|
||||||
|
private readonly logger = new Logger("CalVideoService");
|
||||||
|
constructor(
|
||||||
|
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||||
|
private readonly outputService: OutputBookingsService_2024_08_13
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getRecordings(bookingUid: string) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomName =
|
||||||
|
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
|
||||||
|
undefined;
|
||||||
|
if (!roomName) {
|
||||||
|
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordings = await getRecordingsOfCalVideoByRoomName(roomName);
|
||||||
|
|
||||||
|
if (!recordings || !("data" in recordings)) return [];
|
||||||
|
|
||||||
|
const recordingWithDownloadLink = recordings.data.map((recording) => {
|
||||||
|
return getDownloadLinkOfCalVideoByRecordingId(recording.id)
|
||||||
|
.then((res: { download_link: string } | undefined) => ({
|
||||||
|
id: recording.id,
|
||||||
|
roomName: recording.room_name,
|
||||||
|
startTs: recording.start_ts,
|
||||||
|
status: recording.status,
|
||||||
|
maxParticipants: recording.max_participants,
|
||||||
|
duration: recording.duration,
|
||||||
|
shareToken: recording.share_token,
|
||||||
|
downloadLink: res?.download_link,
|
||||||
|
}))
|
||||||
|
.catch((err: Error) => ({
|
||||||
|
id: recording.id,
|
||||||
|
roomName: recording.room_name,
|
||||||
|
startTs: recording.start_ts,
|
||||||
|
status: recording.status,
|
||||||
|
maxParticipants: recording.max_participants,
|
||||||
|
duration: recording.duration,
|
||||||
|
shareToken: recording.share_token,
|
||||||
|
downloadLink: null,
|
||||||
|
error: err.message,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
const allRecordingsWithDownloadLink = await Promise.all(recordingWithDownloadLink);
|
||||||
|
|
||||||
|
return allRecordingsWithDownloadLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTranscripts(bookingUid: string) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomName =
|
||||||
|
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
if (!roomName) {
|
||||||
|
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(roomName);
|
||||||
|
|
||||||
|
return transcripts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ type OAuthRequestParams = {
|
|||||||
platformBookingUrl: string;
|
platformBookingUrl: string;
|
||||||
platformBookingLocation?: string;
|
platformBookingLocation?: string;
|
||||||
arePlatformEmailsEnabled: boolean;
|
arePlatformEmailsEnabled: boolean;
|
||||||
|
areCalendarEventsEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum Frequency {
|
export enum Frequency {
|
||||||
@@ -185,6 +186,31 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
guests,
|
guests,
|
||||||
location,
|
location,
|
||||||
},
|
},
|
||||||
|
...this.getRoutingFormData(inputBooking.routing),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRoutingFormData(
|
||||||
|
routing:
|
||||||
|
| {
|
||||||
|
teamMemberIds?: number[];
|
||||||
|
responseId?: number;
|
||||||
|
teamMemberEmail?: string;
|
||||||
|
skipContactOwner?: boolean;
|
||||||
|
crmAppSlug?: string;
|
||||||
|
crmOwnerRecordType?: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
) {
|
||||||
|
if (!routing) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
routedTeamMemberIds: routing.teamMemberIds,
|
||||||
|
routingFormResponseId: routing.responseId,
|
||||||
|
teamMemberEmail: routing.teamMemberEmail,
|
||||||
|
skipContactOwner: routing.skipContactOwner,
|
||||||
|
crmAppSlug: routing.crmAppSlug,
|
||||||
|
crmOwnerRecordType: routing.crmOwnerRecordType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +478,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
location,
|
location,
|
||||||
},
|
},
|
||||||
schedulingType: eventType.schedulingType,
|
schedulingType: eventType.schedulingType,
|
||||||
|
...this.getRoutingFormData(inputBooking.routing),
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (timeBetween) {
|
switch (timeBetween) {
|
||||||
@@ -585,6 +612,11 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
||||||
}
|
}
|
||||||
|
if (eventType.seatsPerTimeSlot) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const bookingResponses = safeParse(
|
const bookingResponses = safeParse(
|
||||||
bookingResponsesSchema,
|
bookingResponsesSchema,
|
||||||
@@ -705,6 +737,12 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (booking.status === "CANCELLED") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't cancel booking with uid=${bookingUid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const oAuthClientParams = booking.eventTypeId
|
const oAuthClientParams = booking.eventTypeId
|
||||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
RecurringBookingOutput_2024_08_13,
|
RecurringBookingOutput_2024_08_13,
|
||||||
SeatedAttendee,
|
SeatedAttendee,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { Booking, BookingReference, BookingSeat } from "@calcom/prisma/client";
|
import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||||
|
|
||||||
export const bookingResponsesSchema = z
|
export const bookingResponsesSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -90,7 +90,7 @@ type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
|
|||||||
export class OutputBookingsService_2024_08_13 {
|
export class OutputBookingsService_2024_08_13 {
|
||||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||||
|
|
||||||
getOutputBooking(databaseBooking: DatabaseBooking) {
|
async getOutputBooking(databaseBooking: DatabaseBooking) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
@@ -101,6 +101,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
);
|
);
|
||||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||||
|
const rescheduledToUid = databaseBooking.rescheduled
|
||||||
|
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
@@ -109,8 +112,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
|
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
@@ -137,6 +141,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
updatedAt: databaseBooking.updatedAt,
|
updatedAt: databaseBooking.updatedAt,
|
||||||
rating: databaseBooking.rating,
|
rating: databaseBooking.rating,
|
||||||
icsUid: databaseBooking.iCalUID,
|
icsUid: databaseBooking.iCalUID,
|
||||||
|
rescheduledToUid,
|
||||||
};
|
};
|
||||||
|
|
||||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||||
@@ -146,6 +151,11 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
return bookingTransformed;
|
return bookingTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRescheduledToUid(bookingUid: string) {
|
||||||
|
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||||
|
return rescheduledTo?.uid;
|
||||||
|
}
|
||||||
|
|
||||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||||
if (databaseMetadata === null) return {};
|
if (databaseMetadata === null) return {};
|
||||||
|
|
||||||
@@ -204,8 +214,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
|
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
@@ -244,20 +255,23 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
return bookingTransformed;
|
return bookingTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputCreateSeatedBooking(
|
async getOutputCreateSeatedBooking(
|
||||||
databaseBooking: DatabaseBooking,
|
databaseBooking: DatabaseBooking,
|
||||||
seatUid: string
|
seatUid: string
|
||||||
): CreateSeatedBookingOutput_2024_08_13 {
|
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
||||||
const getSeatedBookingOutput = this.getOutputSeatedBooking(databaseBooking);
|
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
|
||||||
return { ...getSeatedBookingOutput, seatUid };
|
return { ...getSeatedBookingOutput, seatUid };
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||||
|
const rescheduledToUid = databaseBooking.rescheduled
|
||||||
|
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
@@ -266,6 +280,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
|
cancellationReason:
|
||||||
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
start: databaseBooking.startTime,
|
start: databaseBooking.startTime,
|
||||||
end: databaseBooking.endTime,
|
end: databaseBooking.endTime,
|
||||||
@@ -282,6 +298,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
updatedAt: databaseBooking.updatedAt,
|
updatedAt: databaseBooking.updatedAt,
|
||||||
rating: databaseBooking.rating,
|
rating: databaseBooking.rating,
|
||||||
icsUid: databaseBooking.iCalUID,
|
icsUid: databaseBooking.iCalUID,
|
||||||
|
rescheduledToUid,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||||
@@ -374,7 +391,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
start: databaseBooking.startTime,
|
start: databaseBooking.startTime,
|
||||||
end: databaseBooking.endTime,
|
end: databaseBooking.endTime,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export class PlatformBookingsService {
|
|||||||
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
||||||
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
||||||
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
||||||
|
areCalendarEventsEnabled: oAuthClient.areCalendarEventsEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ export class PlatformBookingsService {
|
|||||||
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
||||||
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
||||||
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
||||||
|
areCalendarEventsEnabled: oAuthClient.areCalendarEventsEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||||
@@ -25,8 +27,10 @@ import { Module } from "@nestjs/common";
|
|||||||
SelectedCalendarsRepository,
|
SelectedCalendarsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
CalendarsRepository,
|
CalendarsRepository,
|
||||||
|
BookingsRepository_2024_08_13,
|
||||||
|
BookingReferencesRepository_2024_08_13,
|
||||||
],
|
],
|
||||||
controllers: [CalendarsController],
|
controllers: [CalendarsController],
|
||||||
exports: [CalendarsService],
|
exports: [CalendarsService, GoogleCalendarService],
|
||||||
})
|
})
|
||||||
export class CalendarsModule {}
|
export class CalendarsModule {}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
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 type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
const credentialForCalendarRepositorySelect = Prisma.validator<Prisma.CredentialSelect>()({
|
const credentialForCalendarRepositorySelect = {
|
||||||
id: true,
|
id: true,
|
||||||
appId: true,
|
appId: true,
|
||||||
type: true,
|
type: true,
|
||||||
@@ -16,7 +16,7 @@ const credentialForCalendarRepositorySelect = Prisma.validator<Prisma.Credential
|
|||||||
teamId: true,
|
teamId: true,
|
||||||
key: true,
|
key: true,
|
||||||
invalid: true,
|
invalid: true,
|
||||||
});
|
} satisfies Prisma.CredentialSelect;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CalendarsRepository {
|
export class CalendarsRepository {
|
||||||
|
|||||||
@@ -54,6 +54,23 @@ import {
|
|||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
|
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export interface CalendarState {
|
||||||
|
accessToken: string;
|
||||||
|
origin: string;
|
||||||
|
redir?: string;
|
||||||
|
isDryRun?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarStateSchema = z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
origin: z.string(),
|
||||||
|
redir: z.string().optional(),
|
||||||
|
isDryRun: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => val === "true"),
|
||||||
|
});
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/calendars",
|
path: "/v2/calendars",
|
||||||
version: API_VERSIONS_VALUES,
|
version: API_VERSIONS_VALUES,
|
||||||
@@ -140,7 +157,7 @@ export class CalendarsController {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Get("/:calendar/connect")
|
@Get("/:calendar/connect")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: "Get oAuth connect URL" })
|
@ApiOperation({ summary: "Get OAuth connect URL" })
|
||||||
@ApiQuery({
|
@ApiQuery({
|
||||||
name: "redir",
|
name: "redir",
|
||||||
required: false,
|
required: false,
|
||||||
@@ -181,32 +198,31 @@ export class CalendarsController {
|
|||||||
@Query("code") code: string,
|
@Query("code") code: string,
|
||||||
@Param("calendar") calendar: string
|
@Param("calendar") calendar: string
|
||||||
): Promise<{ url: string }> {
|
): Promise<{ url: string }> {
|
||||||
// state params contains our user access token
|
let stateObj: CalendarState;
|
||||||
const stateParams = new URLSearchParams(state);
|
|
||||||
const { accessToken, origin, redir, isDryRun } = z
|
try {
|
||||||
.object({
|
// First try to parse as JSON
|
||||||
accessToken: z.string(),
|
stateObj = JSON.parse(state) as CalendarState;
|
||||||
origin: z.string(),
|
} catch (e) {
|
||||||
redir: z.string().nullish().optional(),
|
// If JSON parsing fails, try URL params
|
||||||
isDryRun: z.string().nullish().optional(),
|
const stateParams = new URLSearchParams(state);
|
||||||
})
|
|
||||||
.parse({
|
const parsedState = calendarStateSchema.parse({
|
||||||
accessToken: stateParams.get("accessToken"),
|
accessToken: stateParams.get("accessToken"),
|
||||||
origin: stateParams.get("origin"),
|
origin: stateParams.get("origin"),
|
||||||
redir: stateParams.get("redir"),
|
redir: stateParams.get("redir") || undefined,
|
||||||
isDryRun: stateParams.get("isDryRun"),
|
isDryRun: stateParams.get("isDryRun"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stateObj = parsedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken, origin, redir, isDryRun } = stateObj;
|
||||||
switch (calendar) {
|
switch (calendar) {
|
||||||
case OFFICE_365_CALENDAR:
|
case OFFICE_365_CALENDAR:
|
||||||
return await this.outlookService.save(code, accessToken, origin, redir ?? "", isDryRun === "true");
|
return await this.outlookService.save(code, accessToken, origin, redir ?? "", !!isDryRun);
|
||||||
case GOOGLE_CALENDAR:
|
case GOOGLE_CALENDAR:
|
||||||
return await this.googleCalendarService.save(
|
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "", !!isDryRun);
|
||||||
code,
|
|
||||||
accessToken,
|
|
||||||
origin,
|
|
||||||
redir ?? "",
|
|
||||||
isDryRun === "true"
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Invalid calendar type, available calendars are: ",
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import type { CalendarState } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
@@ -23,7 +24,7 @@ const CALENDAR_SCOPES = [
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleCalendarService implements OAuthCalendarApp {
|
export class GoogleCalendarService implements OAuthCalendarApp {
|
||||||
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
public readonly redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||||
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||||
private logger = new Logger("GcalService");
|
private logger = new Logger("GcalService");
|
||||||
|
|
||||||
@@ -65,12 +66,18 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
|||||||
|
|
||||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
||||||
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
||||||
|
const state: CalendarState = {
|
||||||
|
accessToken,
|
||||||
|
origin,
|
||||||
|
redir,
|
||||||
|
isDryRun,
|
||||||
|
};
|
||||||
|
|
||||||
const authUrl = oAuth2Client.generateAuthUrl({
|
const authUrl = oAuth2Client.generateAuthUrl({
|
||||||
access_type: "offline",
|
access_type: "offline",
|
||||||
scope: CALENDAR_SCOPES,
|
scope: CALENDAR_SCOPES,
|
||||||
prompt: "consent",
|
prompt: "consent",
|
||||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
|
state: JSON.stringify(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
return authUrl;
|
return authUrl;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import { CalendarState } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
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";
|
||||||
@@ -60,6 +61,13 @@ export class OutlookService implements OAuthCalendarApp {
|
|||||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
||||||
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
||||||
|
|
||||||
|
const state: CalendarState = {
|
||||||
|
accessToken,
|
||||||
|
origin,
|
||||||
|
redir,
|
||||||
|
isDryRun,
|
||||||
|
};
|
||||||
|
|
||||||
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
||||||
const params = {
|
const params = {
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
@@ -67,7 +75,7 @@ export class OutlookService implements OAuthCalendarApp {
|
|||||||
client_id,
|
client_id,
|
||||||
prompt: "select_account",
|
prompt: "select_account",
|
||||||
redirect_uri: this.redirectUri,
|
redirect_uri: this.redirectUri,
|
||||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
|
state: JSON.stringify(state),
|
||||||
};
|
};
|
||||||
|
|
||||||
const query = stringify(params);
|
const query = stringify(params);
|
||||||
|
|||||||
+26
-5
@@ -18,6 +18,7 @@ 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 { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.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 { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
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";
|
||||||
import {
|
import {
|
||||||
@@ -33,13 +34,19 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
|
Headers,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import {
|
||||||
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
EVENT_TYPE_READ,
|
||||||
|
EVENT_TYPE_WRITE,
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries/event-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -51,7 +58,8 @@ import { PrismaClient } from "@calcom/prisma";
|
|||||||
export class EventTypesController_2024_04_15 {
|
export class EventTypesController_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||||
private readonly prismaReadService: PrismaReadService
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly organizationsRepository: OrganizationsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -109,19 +117,32 @@ export class EventTypesController_2024_04_15 {
|
|||||||
async getPublicEventType(
|
async getPublicEventType(
|
||||||
@Param("username") username: string,
|
@Param("username") username: string,
|
||||||
@Param("eventSlug") eventSlug: string,
|
@Param("eventSlug") eventSlug: string,
|
||||||
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15
|
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15,
|
||||||
|
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||||
): Promise<GetEventTypePublicOutput> {
|
): Promise<GetEventTypePublicOutput> {
|
||||||
try {
|
try {
|
||||||
|
let orgSlug = queryParams.org;
|
||||||
|
|
||||||
|
if (clientId && !orgSlug && username.includes(`-${clientId}`)) {
|
||||||
|
const org = await this.organizationsRepository
|
||||||
|
.findTeamIdAndSlugFromClientId(clientId)
|
||||||
|
.catch(() => null);
|
||||||
|
if (org) {
|
||||||
|
orgSlug = org.slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const event = await getPublicEvent(
|
const event = await getPublicEvent(
|
||||||
username.toLowerCase(),
|
username.toLowerCase(),
|
||||||
eventSlug,
|
eventSlug,
|
||||||
queryParams.isTeamEvent,
|
queryParams.isTeamEvent,
|
||||||
queryParams.org || null,
|
orgSlug ?? null,
|
||||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: event,
|
data: event,
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_20
|
|||||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
@@ -9,7 +10,14 @@ import { UsersModule } from "@/modules/users/users.module";
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
MembershipsModule,
|
||||||
|
TokensModule,
|
||||||
|
UsersModule,
|
||||||
|
SelectedCalendarsModule,
|
||||||
|
OrganizationsModule,
|
||||||
|
],
|
||||||
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
||||||
controllers: [EventTypesController_2024_04_15],
|
controllers: [EventTypesController_2024_04_15],
|
||||||
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
||||||
|
|||||||
+146
-1
@@ -34,6 +34,7 @@ import {
|
|||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
|
GuestsDefaultFieldOutput_2024_06_14,
|
||||||
NameDefaultFieldInput_2024_06_14,
|
NameDefaultFieldInput_2024_06_14,
|
||||||
NotesDefaultFieldInput_2024_06_14,
|
NotesDefaultFieldInput_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
@@ -535,6 +536,7 @@ describe("Event types Endpoints", () => {
|
|||||||
body.lockTimeZoneToggleOnBookingPage
|
body.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
expect(createdEventType.color).toEqual(body.color);
|
expect(createdEventType.color).toEqual(body.color);
|
||||||
|
expect(createdEventType.disableGuests).toEqual(false);
|
||||||
|
|
||||||
const expectedBookingFields = [
|
const expectedBookingFields = [
|
||||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||||
@@ -962,6 +964,10 @@ describe("Event types Endpoints", () => {
|
|||||||
title: newTitle,
|
title: newTitle,
|
||||||
scheduleId: secondSchedule.id,
|
scheduleId: secondSchedule.id,
|
||||||
lengthInMinutesOptions: [15, 30],
|
lengthInMinutesOptions: [15, 30],
|
||||||
|
calVideoSettings: {
|
||||||
|
disableRecordingForGuests: true,
|
||||||
|
disableRecordingForOrganizer: true,
|
||||||
|
},
|
||||||
bookingFields: [
|
bookingFields: [
|
||||||
nameBookingField,
|
nameBookingField,
|
||||||
{
|
{
|
||||||
@@ -1037,6 +1043,7 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(updatedEventType.description).toEqual(eventType.description);
|
expect(updatedEventType.description).toEqual(eventType.description);
|
||||||
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||||
expect(updatedEventType.locations).toEqual(eventType.locations);
|
expect(updatedEventType.locations).toEqual(eventType.locations);
|
||||||
|
expect(updatedEventType.disableGuests).toEqual(false);
|
||||||
|
|
||||||
const expectedBookingFields = [
|
const expectedBookingFields = [
|
||||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||||
@@ -1082,6 +1089,12 @@ describe("Event types Endpoints", () => {
|
|||||||
body.lockTimeZoneToggleOnBookingPage
|
body.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
expect(updatedEventType.color).toEqual(body.color);
|
expect(updatedEventType.color).toEqual(body.color);
|
||||||
|
expect(updatedEventType.calVideoSettings?.disableRecordingForGuests).toEqual(
|
||||||
|
body.calVideoSettings?.disableRecordingForGuests
|
||||||
|
);
|
||||||
|
expect(updatedEventType.calVideoSettings?.disableRecordingForOrganizer).toEqual(
|
||||||
|
body.calVideoSettings?.disableRecordingForOrganizer
|
||||||
|
);
|
||||||
|
|
||||||
eventType.title = newTitle;
|
eventType.title = newTitle;
|
||||||
eventType.scheduleId = secondSchedule.id;
|
eventType.scheduleId = secondSchedule.id;
|
||||||
@@ -1708,7 +1721,7 @@ describe("Event types Endpoints", () => {
|
|||||||
description: "undefined booking fields",
|
description: "undefined booking fields",
|
||||||
length: 40,
|
length: 40,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
slug: "undefined-booking-fields",
|
slug: `undefined-booking-fields-${randomString()}`,
|
||||||
locations: [],
|
locations: [],
|
||||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
};
|
};
|
||||||
@@ -1738,6 +1751,14 @@ describe("Event types Endpoints", () => {
|
|||||||
disableOnPrefill: false,
|
disableOnPrefill: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
disableOnPrefill: false,
|
||||||
|
hidden: true,
|
||||||
|
isDefault: true,
|
||||||
|
required: false,
|
||||||
|
slug: "attendeePhoneNumber",
|
||||||
|
type: "phone",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
type: "radioInput",
|
type: "radioInput",
|
||||||
@@ -1841,6 +1862,130 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(slug2).not.toEqual(slug);
|
expect(slug2).not.toEqual(slug);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create event type with cal video settings", async () => {
|
||||||
|
const body: CreateEventTypeInput_2024_06_14 = {
|
||||||
|
title: "event type with cal video settings",
|
||||||
|
slug: "event-type-with-cal-video-settings",
|
||||||
|
lengthInMinutes: 60,
|
||||||
|
calVideoSettings: {
|
||||||
|
disableRecordingForGuests: true,
|
||||||
|
disableRecordingForOrganizer: true,
|
||||||
|
},
|
||||||
|
locations: [
|
||||||
|
{
|
||||||
|
type: "integration",
|
||||||
|
integration: "cal-video",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(body.title);
|
||||||
|
expect(createdEventType.locations).toEqual(body.locations);
|
||||||
|
expect(createdEventType.calVideoSettings?.disableRecordingForGuests).toEqual(true);
|
||||||
|
expect(createdEventType.calVideoSettings?.disableRecordingForOrganizer).toEqual(true);
|
||||||
|
firstCreatedEventType = responseBody.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toggle disable guests", () => {
|
||||||
|
let eventTypeWithGuestsDisabledId: number;
|
||||||
|
|
||||||
|
it("should create an event type with guests disabled", async () => {
|
||||||
|
const body: CreateEventTypeInput_2024_06_14 = {
|
||||||
|
title: "Coding class guests disabled",
|
||||||
|
slug: "coding-class-guests-disabled",
|
||||||
|
description: "Let's learn how to code like a pro.",
|
||||||
|
lengthInMinutes: 60,
|
||||||
|
disableGuests: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(body.title);
|
||||||
|
expect(createdEventType.description).toEqual(body.description);
|
||||||
|
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
||||||
|
|
||||||
|
expect(createdEventType.disableGuests).toEqual(true);
|
||||||
|
const guestsBookingField = createdEventType.bookingFields.find(
|
||||||
|
(field) => field.slug === "guests"
|
||||||
|
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
|
||||||
|
expect(guestsBookingField?.hidden).toEqual(true);
|
||||||
|
|
||||||
|
eventTypeWithGuestsDisabledId = createdEventType.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update an event type with guests enabled", async () => {
|
||||||
|
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||||
|
disableGuests: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventTypeWithGuestsDisabledId}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
|
||||||
|
expect(updatedEventType.disableGuests).toEqual(false);
|
||||||
|
const guestsBookingField = updatedEventType.bookingFields.find(
|
||||||
|
(field) => field.slug === "guests"
|
||||||
|
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
|
||||||
|
expect(guestsBookingField?.hidden).toEqual(false);
|
||||||
|
|
||||||
|
eventTypeWithGuestsDisabledId = updatedEventType.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update an event type with guests disabled", async () => {
|
||||||
|
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||||
|
disableGuests: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventTypeWithGuestsDisabledId}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
|
||||||
|
expect(updatedEventType.disableGuests).toEqual(true);
|
||||||
|
const guestsBookingField = updatedEventType.bookingFields.find(
|
||||||
|
(field) => field.slug === "guests"
|
||||||
|
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
|
||||||
|
expect(guestsBookingField?.hidden).toEqual(true);
|
||||||
|
|
||||||
|
eventTypeWithGuestsDisabledId = updatedEventType.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (eventTypeWithGuestsDisabledId) {
|
||||||
|
await eventTypesRepositoryFixture.delete(eventTypeWithGuestsDisabledId);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -12,13 +12,20 @@ export class EventTypesRepository_2024_06_14 {
|
|||||||
userId: number,
|
userId: number,
|
||||||
body: Omit<InputEventTransformed_2024_06_14, "destinationCalendar">
|
body: Omit<InputEventTransformed_2024_06_14, "destinationCalendar">
|
||||||
) {
|
) {
|
||||||
|
const { calVideoSettings, ...restBody } = body;
|
||||||
|
|
||||||
return this.dbWrite.prisma.eventType.create({
|
return this.dbWrite.prisma.eventType.create({
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...restBody,
|
||||||
userId,
|
userId,
|
||||||
locations: body.locations,
|
locations: body.locations,
|
||||||
bookingFields: body.bookingFields,
|
bookingFields: body.bookingFields,
|
||||||
users: { connect: { id: userId } },
|
users: { connect: { id: userId } },
|
||||||
|
...(calVideoSettings && {
|
||||||
|
calVideoSettings: {
|
||||||
|
create: calVideoSettings,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,7 +78,7 @@ export class EventTypesRepository_2024_06_14 {
|
|||||||
async getEventTypeById(eventTypeId: number) {
|
async getEventTypeById(eventTypeId: number) {
|
||||||
return this.dbRead.prisma.eventType.findUnique({
|
return this.dbRead.prisma.eventType.findUnique({
|
||||||
where: { id: eventTypeId },
|
where: { id: eventTypeId },
|
||||||
include: { users: true, schedule: true, destinationCalendar: true },
|
include: { users: true, schedule: true, destinationCalendar: true, calVideoSettings: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-2
@@ -32,6 +32,7 @@ import { validateCustomEventName, EventTypeMetaDataSchema } from "@calcom/platfo
|
|||||||
import {
|
import {
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
DestinationCalendar_2024_06_14,
|
DestinationCalendar_2024_06_14,
|
||||||
|
InputBookingField_2024_06_14,
|
||||||
OutputUnknownLocation_2024_06_14,
|
OutputUnknownLocation_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
@@ -117,16 +118,23 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
seats,
|
seats,
|
||||||
customName,
|
customName,
|
||||||
useDestinationCalendarEmail,
|
useDestinationCalendarEmail,
|
||||||
|
disableGuests,
|
||||||
...rest
|
...rest
|
||||||
} = inputEventType;
|
} = inputEventType;
|
||||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||||
|
|
||||||
const locationsTransformed = locations?.length ? this.transformInputLocations(locations) : undefined;
|
const locationsTransformed = locations?.length ? this.transformInputLocations(locations) : undefined;
|
||||||
|
|
||||||
|
const effectiveBookingFields =
|
||||||
|
disableGuests !== undefined
|
||||||
|
? this.getBookingFieldsWithGuestsToggled(bookingFields, disableGuests)
|
||||||
|
: bookingFields;
|
||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...rest,
|
...rest,
|
||||||
length: lengthInMinutes,
|
length: lengthInMinutes,
|
||||||
locations: locationsTransformed,
|
locations: locationsTransformed,
|
||||||
bookingFields: this.transformInputBookingFields(bookingFields),
|
bookingFields: this.transformInputBookingFields(effectiveBookingFields),
|
||||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||||
durationLimits: bookingLimitsDuration
|
durationLimits: bookingLimitsDuration
|
||||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||||
@@ -167,6 +175,7 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
seats,
|
seats,
|
||||||
customName,
|
customName,
|
||||||
useDestinationCalendarEmail,
|
useDestinationCalendarEmail,
|
||||||
|
disableGuests,
|
||||||
...rest
|
...rest
|
||||||
} = inputEventType;
|
} = inputEventType;
|
||||||
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId);
|
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId);
|
||||||
@@ -176,11 +185,18 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
|
|
||||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||||
|
|
||||||
|
const effectiveBookingFields =
|
||||||
|
disableGuests !== undefined
|
||||||
|
? this.getBookingFieldsWithGuestsToggled(bookingFields, disableGuests)
|
||||||
|
: bookingFields;
|
||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...rest,
|
...rest,
|
||||||
length: lengthInMinutes,
|
length: lengthInMinutes,
|
||||||
locations: locations ? this.transformInputLocations(locations) : undefined,
|
locations: locations ? this.transformInputLocations(locations) : undefined,
|
||||||
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
|
bookingFields: effectiveBookingFields
|
||||||
|
? this.transformInputBookingFields(effectiveBookingFields)
|
||||||
|
: undefined,
|
||||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||||
durationLimits: bookingLimitsDuration
|
durationLimits: bookingLimitsDuration
|
||||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||||
@@ -206,6 +222,27 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
return eventType;
|
return eventType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBookingFieldsWithGuestsToggled(
|
||||||
|
bookingFields: InputBookingField_2024_06_14[] | undefined,
|
||||||
|
hideGuests: boolean
|
||||||
|
) {
|
||||||
|
const toggledGuestsBookingField: InputBookingField_2024_06_14 = { slug: "guests", hidden: hideGuests };
|
||||||
|
if (!bookingFields) {
|
||||||
|
return [toggledGuestsBookingField];
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingFieldsCopy = [...bookingFields];
|
||||||
|
|
||||||
|
const guestsBookingField = bookingFieldsCopy.find((field) => "slug" in field && field.slug === "guests");
|
||||||
|
if (guestsBookingField) {
|
||||||
|
Object.assign(guestsBookingField, { hidden: hideGuests });
|
||||||
|
return bookingFieldsCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
bookingFieldsCopy.push(toggledGuestsBookingField);
|
||||||
|
return bookingFieldsCopy;
|
||||||
|
}
|
||||||
|
|
||||||
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
|
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
|
||||||
return transformLocationsApiToInternal(inputLocations);
|
return transformLocationsApiToInternal(inputLocations);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -15,7 +15,7 @@ import {
|
|||||||
BookingFieldSchema,
|
BookingFieldSchema,
|
||||||
} from "@/ee/event-types/event-types_2024_06_14/transformers";
|
} from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
|
import type { EventType, User, Schedule, DestinationCalendar, CalVideoSettings } from "@prisma/client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
userMetadata,
|
userMetadata,
|
||||||
@@ -37,6 +37,7 @@ type EventTypeRelations = {
|
|||||||
users: User[];
|
users: User[];
|
||||||
schedule: Schedule | null;
|
schedule: Schedule | null;
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
destinationCalendar?: DestinationCalendar | null;
|
||||||
|
calVideoSettings?: CalVideoSettings | null;
|
||||||
};
|
};
|
||||||
export type DatabaseEventType = Omit<EventType, "allowReschedulingCancelledBookings"> & EventTypeRelations;
|
export type DatabaseEventType = Omit<EventType, "allowReschedulingCancelledBookings"> & EventTypeRelations;
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ type Input = Pick<
|
|||||||
| "useEventTypeDestinationCalendarEmail"
|
| "useEventTypeDestinationCalendarEmail"
|
||||||
| "hideCalendarEventDetails"
|
| "hideCalendarEventDetails"
|
||||||
| "hideOrganizerEmail"
|
| "hideOrganizerEmail"
|
||||||
|
| "calVideoSettings"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -124,6 +126,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
useEventTypeDestinationCalendarEmail,
|
useEventTypeDestinationCalendarEmail,
|
||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
|
calVideoSettings,
|
||||||
} = databaseEventType;
|
} = databaseEventType;
|
||||||
|
|
||||||
const locations = this.transformLocations(databaseEventType.locations);
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
@@ -199,6 +202,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail,
|
useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail,
|
||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
|
calVideoSettings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -46,11 +46,10 @@ export class SchedulesController_2024_04_15 {
|
|||||||
@Body() bodySchedule: CreateScheduleInput_2024_04_15
|
@Body() bodySchedule: CreateScheduleInput_2024_04_15
|
||||||
): Promise<CreateScheduleOutput_2024_04_15> {
|
): 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);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: scheduleFormatted,
|
data: schedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +59,10 @@ export class SchedulesController_2024_04_15 {
|
|||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
|
): 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
|
|
||||||
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: scheduleFormatted,
|
data: schedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,23 +73,25 @@ export class SchedulesController_2024_04_15 {
|
|||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
): Promise<GetScheduleOutput_2024_04_15> {
|
): 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);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: scheduleFormatted,
|
data: schedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
|
||||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
const schedules = await this.schedulesService.getUserSchedules(
|
||||||
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
user.id,
|
||||||
|
user.timeZone,
|
||||||
|
user.defaultScheduleId
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: schedulesFormatted,
|
data: schedules,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04
|
|||||||
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 type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesRepository_2024_04_15 {
|
export class SchedulesRepository_2024_04_15 {
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
|
||||||
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
|
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
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 type { ScheduleWithAvailabilities } from "@calcom/platform-libraries-0.0.2";
|
import { updateSchedule } from "@calcom/platform-libraries/schedules";
|
||||||
import { updateScheduleHandler } from "@calcom/platform-libraries-0.0.2";
|
import { ScheduleRepository } from "@calcom/platform-libraries/schedules";
|
||||||
import {
|
|
||||||
transformWorkingHoursForClient,
|
|
||||||
transformAvailabilityForClient,
|
|
||||||
transformDateOverridesForClient,
|
|
||||||
} from "@calcom/platform-libraries-0.0.2";
|
|
||||||
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesService_2024_04_15 {
|
export class SchedulesService_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
|
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
|
||||||
private readonly usersRepository: UsersRepository
|
private readonly usersRepository: UsersRepository,
|
||||||
|
private readonly dbWrite: PrismaWriteService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUserDefaultSchedule(userId: number, timeZone: string) {
|
async createUserDefaultSchedule(userId: number, timeZone: string) {
|
||||||
@@ -48,7 +44,9 @@ export class SchedulesService_2024_04_15 {
|
|||||||
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
|
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdSchedule;
|
const formattedSchedule = await this.getUserSchedule(userId, createdSchedule.id);
|
||||||
|
|
||||||
|
return formattedSchedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserScheduleDefault(userId: number) {
|
async getUserScheduleDefault(userId: number) {
|
||||||
@@ -56,11 +54,29 @@ export class SchedulesService_2024_04_15 {
|
|||||||
|
|
||||||
if (!user?.defaultScheduleId) return null;
|
if (!user?.defaultScheduleId) return null;
|
||||||
|
|
||||||
return this.schedulesRepository.getScheduleById(user.defaultScheduleId);
|
return await ScheduleRepository.findDetailedScheduleById({
|
||||||
|
scheduleId: user.defaultScheduleId,
|
||||||
|
isManagedEventType: undefined,
|
||||||
|
userId,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
defaultScheduleId: user.defaultScheduleId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSchedule(userId: number, scheduleId: number) {
|
async getUserSchedule(userId: number, scheduleId: number) {
|
||||||
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
const user = await this.usersRepository.findById(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with ID=${userId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSchedule = await ScheduleRepository.findDetailedScheduleById({
|
||||||
|
scheduleId: scheduleId,
|
||||||
|
isManagedEventType: undefined,
|
||||||
|
userId,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
defaultScheduleId: user.defaultScheduleId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!existingSchedule) {
|
if (!existingSchedule) {
|
||||||
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
@@ -71,8 +87,8 @@ export class SchedulesService_2024_04_15 {
|
|||||||
return existingSchedule;
|
return existingSchedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSchedules(userId: number) {
|
async getUserSchedules(userId: number, timeZone: string, defaultScheduleId: number | null) {
|
||||||
return this.schedulesRepository.getSchedulesByUserId(userId);
|
return ScheduleRepository.findManyDetailedScheduleByUserId({ userId, timeZone, defaultScheduleId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserSchedule(
|
async updateUserSchedule(
|
||||||
@@ -89,7 +105,6 @@ export class SchedulesService_2024_04_15 {
|
|||||||
this.checkUserOwnsSchedule(user.id, existingSchedule);
|
this.checkUserOwnsSchedule(user.id, existingSchedule);
|
||||||
|
|
||||||
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
|
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
|
||||||
const scheduleFormatted = await this.formatScheduleForAtom(user, schedule);
|
|
||||||
|
|
||||||
if (!bodySchedule.schedule) {
|
if (!bodySchedule.schedule) {
|
||||||
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
|
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
|
||||||
@@ -97,12 +112,16 @@ export class SchedulesService_2024_04_15 {
|
|||||||
// and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect
|
// and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect
|
||||||
// schedule that would be passed by the web app. If we don't, then updating schedule name will erase
|
// schedule that would be passed by the web app. If we don't, then updating schedule name will erase
|
||||||
// schedule.
|
// schedule.
|
||||||
bodySchedule.schedule = scheduleFormatted.availability;
|
bodySchedule.schedule = schedule.availability;
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateScheduleHandler({
|
return updateSchedule({
|
||||||
input: { scheduleId: Number(scheduleId), ...bodySchedule },
|
input: {
|
||||||
ctx: { user },
|
scheduleId: Number(scheduleId),
|
||||||
|
...bodySchedule,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,44 +137,6 @@ export class SchedulesService_2024_04_15 {
|
|||||||
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async formatScheduleForAtom(user: User, schedule: ScheduleWithAvailabilities): Promise<ScheduleOutput> {
|
|
||||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
|
||||||
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async formatSchedulesForAtom(
|
|
||||||
user: User,
|
|
||||||
schedules: ScheduleWithAvailabilities[]
|
|
||||||
): Promise<ScheduleOutput[]> {
|
|
||||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
|
||||||
return Promise.all(
|
|
||||||
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async transformScheduleForAtom(
|
|
||||||
schedule: ScheduleWithAvailabilities,
|
|
||||||
userSchedulesCount: number,
|
|
||||||
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
|
|
||||||
): Promise<ScheduleOutput> {
|
|
||||||
const timeZone = schedule.timeZone || user.timeZone;
|
|
||||||
const defaultSchedule = await this.getUserScheduleDefault(user.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: schedule.id,
|
|
||||||
name: schedule.name,
|
|
||||||
isManaged: schedule.userId !== user.id,
|
|
||||||
workingHours: transformWorkingHoursForClient(schedule),
|
|
||||||
schedule: schedule.availability,
|
|
||||||
availability: transformAvailabilityForClient(schedule),
|
|
||||||
timeZone,
|
|
||||||
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
|
|
||||||
isDefault: defaultSchedule?.id === schedule.id,
|
|
||||||
isLastSchedule: userSchedulesCount <= 1,
|
|
||||||
readOnly: schedule.userId !== user.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
||||||
if (userId !== schedule.userId) {
|
if (userId !== schedule.userId) {
|
||||||
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}`);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@@ -39,6 +40,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
|||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -46,6 +48,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(400).json({
|
response.status(400).json({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@@ -16,6 +17,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
|||||||
const statusCode = exception.getStatus();
|
const statusCode = exception.getStatus();
|
||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -23,6 +25,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(statusCode).json({
|
response.status(statusCode).json({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||||
@@ -43,6 +44,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
|||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`PrismaError: ${error.message}`, {
|
this.logger.error(`PrismaError: ${error.message}`, {
|
||||||
error,
|
error,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -50,6 +52,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = "There was an error, please try again later.";
|
let message = "There was an error, please try again later.";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@@ -6,6 +7,100 @@ import { ERROR_STATUS } from "@calcom/platform-constants";
|
|||||||
import { TRPCError } from "@calcom/platform-libraries";
|
import { TRPCError } from "@calcom/platform-libraries";
|
||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export type TRPCErrorCode = TRPCError["code"];
|
||||||
|
|
||||||
|
export interface ErrorDefinition {
|
||||||
|
statusCode: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the specific TRPC error codes
|
||||||
|
export const TRPC_ERROR_CODE = {
|
||||||
|
UNAUTHORIZED: "UNAUTHORIZED",
|
||||||
|
FORBIDDEN: "FORBIDDEN",
|
||||||
|
NOT_FOUND: "NOT_FOUND",
|
||||||
|
INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR",
|
||||||
|
BAD_REQUEST: "BAD_REQUEST",
|
||||||
|
CONFLICT: "CONFLICT",
|
||||||
|
TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS",
|
||||||
|
PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
|
||||||
|
CLIENT_CLOSED_REQUEST: "CLIENT_CLOSED_REQUEST",
|
||||||
|
METHOD_NOT_SUPPORTED: "METHOD_NOT_SUPPORTED",
|
||||||
|
NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
|
||||||
|
PRECONDITION_FAILED: "PRECONDITION_FAILED",
|
||||||
|
TIMEOUT: "TIMEOUT",
|
||||||
|
UNPROCESSABLE_CONTENT: "UNPROCESSABLE_CONTENT",
|
||||||
|
PARSE_ERROR: "PARSE_ERROR",
|
||||||
|
} as const; // `as const` ensures literal types, improving type inference
|
||||||
|
|
||||||
|
// Map TRPC error codes to their HTTP status codes and default messages
|
||||||
|
export const TRPC_ERROR_MAP: Record<(typeof TRPC_ERROR_CODE)[keyof typeof TRPC_ERROR_CODE], ErrorDefinition> =
|
||||||
|
{
|
||||||
|
[TRPC_ERROR_CODE.UNAUTHORIZED]: {
|
||||||
|
statusCode: 401,
|
||||||
|
message: "You are not authorized to access this resource",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.FORBIDDEN]: {
|
||||||
|
statusCode: 403,
|
||||||
|
message: "You don't have necessary permissions to access this resource",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.NOT_FOUND]: {
|
||||||
|
statusCode: 404,
|
||||||
|
message: "The requested resource was not found",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.INTERNAL_SERVER_ERROR]: {
|
||||||
|
statusCode: 500,
|
||||||
|
message: "An unexpected error occurred on the server. Please try again later",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.BAD_REQUEST]: {
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request: Please check your request data and try again",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.CONFLICT]: {
|
||||||
|
statusCode: 409,
|
||||||
|
message: "Could not process the request due to a resource conflict with the current state",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.TOO_MANY_REQUESTS]: {
|
||||||
|
statusCode: 429,
|
||||||
|
message: "You have exceeded the allowed number of requests",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.PAYLOAD_TOO_LARGE]: {
|
||||||
|
statusCode: 413,
|
||||||
|
message: "The request payload is too large",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.CLIENT_CLOSED_REQUEST]: {
|
||||||
|
statusCode: 499,
|
||||||
|
message: "The client closed the connection before the server could finish processing the request",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.METHOD_NOT_SUPPORTED]: {
|
||||||
|
statusCode: 405,
|
||||||
|
message: "The requested method is not supported for this resource",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.NOT_IMPLEMENTED]: {
|
||||||
|
statusCode: 501,
|
||||||
|
message: "The requested method is not implemented on the server",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.PRECONDITION_FAILED]: {
|
||||||
|
statusCode: 412,
|
||||||
|
message: "The server does not meet one of the preconditions that the requester put on the request",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.TIMEOUT]: {
|
||||||
|
statusCode: 408,
|
||||||
|
message: "The request took too long to complete. Please try again later",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.UNPROCESSABLE_CONTENT]: {
|
||||||
|
statusCode: 422,
|
||||||
|
message:
|
||||||
|
"The server was unable to process the request because the request payload is semantically incorrect or because the request is syntactically incorrect",
|
||||||
|
},
|
||||||
|
[TRPC_ERROR_CODE.PARSE_ERROR]: {
|
||||||
|
statusCode: 400,
|
||||||
|
message: "The request could not be parsed due to invalid syntax.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- TRPCExceptionFilter (updated) ---
|
||||||
|
|
||||||
@Catch(TRPCError)
|
@Catch(TRPCError)
|
||||||
export class TRPCExceptionFilter implements ExceptionFilter {
|
export class TRPCExceptionFilter implements ExceptionFilter {
|
||||||
private readonly logger = new Logger("TRPCExceptionFilter");
|
private readonly logger = new Logger("TRPCExceptionFilter");
|
||||||
@@ -15,81 +110,18 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
|||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
const request = ctx.getRequest<Request>();
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
let statusCode = 500;
|
const errorDefinition = TRPC_ERROR_MAP[exception.code as keyof typeof TRPC_ERROR_CODE] || {
|
||||||
let errorMessage = exception.message;
|
statusCode: 500,
|
||||||
|
message: exception.message,
|
||||||
|
};
|
||||||
|
|
||||||
switch (exception.code) {
|
const statusCode = errorDefinition.statusCode;
|
||||||
case "UNAUTHORIZED":
|
const errorMessage = errorDefinition.message;
|
||||||
statusCode = 401;
|
|
||||||
errorMessage = "You are not authorized to access this resource";
|
|
||||||
break;
|
|
||||||
case "FORBIDDEN":
|
|
||||||
statusCode = 403;
|
|
||||||
errorMessage = "You don't have necessary permissions to access this resource";
|
|
||||||
break;
|
|
||||||
case "NOT_FOUND":
|
|
||||||
statusCode = 404;
|
|
||||||
errorMessage = "The requested resource was not found";
|
|
||||||
break;
|
|
||||||
case "INTERNAL_SERVER_ERROR":
|
|
||||||
statusCode = 500;
|
|
||||||
errorMessage = "An unexpected error occurred on the server. Please try again later";
|
|
||||||
break;
|
|
||||||
case "BAD_REQUEST":
|
|
||||||
statusCode = 400;
|
|
||||||
errorMessage = "Bad request: Please check your request data and try again";
|
|
||||||
break;
|
|
||||||
case "CONFLICT":
|
|
||||||
statusCode = 409;
|
|
||||||
errorMessage = "Could not process the request due to a resource conflict with the current state";
|
|
||||||
break;
|
|
||||||
case "TOO_MANY_REQUESTS":
|
|
||||||
statusCode = 429;
|
|
||||||
errorMessage = "You have exceeded the allowed number of requests";
|
|
||||||
break;
|
|
||||||
case "PAYLOAD_TOO_LARGE":
|
|
||||||
statusCode = 413;
|
|
||||||
errorMessage = "The request payload is too large";
|
|
||||||
break;
|
|
||||||
case "CLIENT_CLOSED_REQUEST":
|
|
||||||
statusCode = 499;
|
|
||||||
errorMessage =
|
|
||||||
"The client closed the connection before the server could finish processing the request";
|
|
||||||
break;
|
|
||||||
case "METHOD_NOT_SUPPORTED":
|
|
||||||
statusCode = 405;
|
|
||||||
errorMessage = "The requested method is not supported for this resource";
|
|
||||||
case "NOT_IMPLEMENTED":
|
|
||||||
statusCode = 501;
|
|
||||||
errorMessage = "The requested method is not implemented on the server";
|
|
||||||
break;
|
|
||||||
case "PRECONDITION_FAILED":
|
|
||||||
statusCode = 412;
|
|
||||||
errorMessage =
|
|
||||||
"The server does not meet one of the preconditions that the requester put on the request";
|
|
||||||
break;
|
|
||||||
case "TIMEOUT":
|
|
||||||
statusCode = 408;
|
|
||||||
errorMessage = "The request took too long to complete. Please try again later";
|
|
||||||
break;
|
|
||||||
case "UNPROCESSABLE_CONTENT":
|
|
||||||
statusCode = 422;
|
|
||||||
errorMessage =
|
|
||||||
"The server was unable to process the request because the request payload is semantically incorrect or because the request is syntactically incorrect";
|
|
||||||
break;
|
|
||||||
case "PARSE_ERROR":
|
|
||||||
statusCode = 400;
|
|
||||||
errorMessage = "The request could not be parsed due to invalid syntax.";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
statusCode = 500;
|
|
||||||
errorMessage = exception.message;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -97,13 +129,14 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(statusCode).json({
|
response.status(statusCode).json({
|
||||||
status: ERROR_STATUS,
|
status: ERROR_STATUS,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
path: request.url,
|
path: request.url,
|
||||||
error: { code: exception.name, message: errorMessage },
|
error: { code: exception.name, message: errorMessage }, // exception.name is usually "TRPCError"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||||
@@ -18,6 +19,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
|||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
|
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
this.logger.error(`ZodError: ${error.message}`, {
|
this.logger.error(`ZodError: ${error.message}`, {
|
||||||
error,
|
error,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@@ -25,6 +27,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
|||||||
url: request.url,
|
url: request.url,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
requestId,
|
requestId,
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(HttpStatus.BAD_REQUEST).json({
|
response.status(HttpStatus.BAD_REQUEST).json({
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export enum AuthMethods {
|
|||||||
"ACCESS_TOKEN" = "access-token",
|
"ACCESS_TOKEN" = "access-token",
|
||||||
"OAUTH_CLIENT" = "oauth-client",
|
"OAUTH_CLIENT" = "oauth-client",
|
||||||
"NEXT_AUTH" = "next-auth",
|
"NEXT_AUTH" = "next-auth",
|
||||||
|
"THIRD_PARTY_ACCESS_TOKEN" = "third_party_access_token",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ export enum Locales {
|
|||||||
TA = "ta",
|
TA = "ta",
|
||||||
UK = "uk",
|
UK = "uk",
|
||||||
ZH_TW = "zh-TW",
|
ZH_TW = "zh-TW",
|
||||||
|
BN = "bn",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
import { ApiAuthGuardUser } from "../modules/auth/strategies/api-auth/api-auth.strategy";
|
||||||
|
|
||||||
|
export interface UserContext {
|
||||||
|
userId?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
organizationId?: string;
|
||||||
|
teamId?: string;
|
||||||
|
userOrgId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractUserContext(request: Request): UserContext {
|
||||||
|
const context: UserContext = {};
|
||||||
|
|
||||||
|
const user = (request as any).user as ApiAuthGuardUser | undefined;
|
||||||
|
if (user) {
|
||||||
|
context.userId = String(user.id);
|
||||||
|
context.userEmail = user.email;
|
||||||
|
if (user.profiles?.[0]?.organizationId) {
|
||||||
|
context.userOrgId = String(user.profiles?.[0].organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizationId = (request as any).organizationId as number | undefined;
|
||||||
|
if (organizationId) {
|
||||||
|
context.organizationId = String(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = (request as any).team as Team | undefined;
|
||||||
|
if (team) {
|
||||||
|
context.teamId = String(team.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
|
import { PrismaSelectedSlotsRepository } from "@/lib/repositories/prisma-selected-slots.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
|
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [
|
||||||
|
PrismaOOORepository,
|
||||||
|
PrismaScheduleRepository,
|
||||||
|
PrismaBookingRepository,
|
||||||
|
PrismaSelectedSlotsRepository,
|
||||||
|
PrismaUserRepository,
|
||||||
|
PrismaEventTypeRepository,
|
||||||
|
PrismaRoutingFormResponseRepository,
|
||||||
|
PrismaTeamRepository,
|
||||||
|
AvailableSlotsService,
|
||||||
|
],
|
||||||
|
exports: [AvailableSlotsService],
|
||||||
|
})
|
||||||
|
export class AvailableSlotsModule {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaBookingRepository as PrismaBookingRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaBookingRepository extends PrismaBookingRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaEventTypeRepository as PrismaEventTypeRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaEventTypeRepository extends PrismaEventTypeRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaOOORepository as PrismaOOORepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaOOORepository extends PrismaOOORepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaRoutingFormResponseRepository as PrismaRoutingFormResponseRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaRoutingFormResponseRepository extends PrismaRoutingFormResponseRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaScheduleRepository as PrismaScheduleRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaScheduleRepository extends PrismaScheduleRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaSelectedSlotsRepository as PrismaSelectedSlotsRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaSelectedSlotsRepository extends PrismaSelectedSlotsRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaTeamRepository as PrismaTeamRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaTeamRepository extends PrismaTeamRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaUserRepository as PrismaUserRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaUserRepository extends PrismaUserRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
|
import { PrismaSelectedSlotsRepository } from "@/lib/repositories/prisma-selected-slots.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||||
|
constructor(
|
||||||
|
private readonly oooRepoDependency: PrismaOOORepository,
|
||||||
|
private readonly scheduleRepoDependency: PrismaScheduleRepository,
|
||||||
|
private readonly teamRepository: PrismaTeamRepository,
|
||||||
|
private readonly routingFormResponseRepository: PrismaRoutingFormResponseRepository,
|
||||||
|
private readonly bookingRepository: PrismaBookingRepository,
|
||||||
|
private readonly selectedSlotsRepository: PrismaSelectedSlotsRepository,
|
||||||
|
private readonly eventTypeRepository: PrismaEventTypeRepository,
|
||||||
|
private readonly userRepository: PrismaUserRepository
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
oooRepo: oooRepoDependency,
|
||||||
|
scheduleRepo: scheduleRepoDependency,
|
||||||
|
teamRepo: teamRepository,
|
||||||
|
routingFormResponseRepo: routingFormResponseRepository,
|
||||||
|
bookingRepo: bookingRepository,
|
||||||
|
selectedSlotsRepo: selectedSlotsRepository,
|
||||||
|
eventTypeRepo: eventTypeRepository,
|
||||||
|
userRepo: userRepository,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,12 +58,6 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
|
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
|
||||||
const response = context.switchToHttp().getResponse<Response>();
|
const response = context.switchToHttp().getResponse<Response>();
|
||||||
const tracker = await this.getTracker(request);
|
const tracker = await this.getTracker(request);
|
||||||
this.logger.verbose(
|
|
||||||
`Tracker "${tracker}" generated based on: Bearer token "${request.get(
|
|
||||||
"Authorization"
|
|
||||||
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${IP}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (throttleOptions) {
|
if (throttleOptions) {
|
||||||
return this.handleApiEndpointThrottle(tracker, throttleOptions, response);
|
return this.handleApiEndpointThrottle(tracker, throttleOptions, response);
|
||||||
}
|
}
|
||||||
@@ -104,9 +98,9 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
|
|
||||||
private async handleNonApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
private async handleNonApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
||||||
const rateLimit = this.getDefaultRateLimit(tracker);
|
const rateLimit = this.getDefaultRateLimit(tracker);
|
||||||
this.logger.verbose(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
|
/*this.logger.verbose(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
|
||||||
${JSON.stringify(rateLimit, null, 2)}
|
${JSON.stringify(rateLimit, null, 2)}
|
||||||
`);
|
`);*/
|
||||||
|
|
||||||
const { isBlocked } = await this.incrementRateLimit(tracker, rateLimit, response);
|
const { isBlocked } = await this.incrementRateLimit(tracker, rateLimit, response);
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
@@ -150,9 +144,9 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
|
|
||||||
const cachedRateLimits = await this.storageService.redis.get(cacheKey);
|
const cachedRateLimits = await this.storageService.redis.get(cacheKey);
|
||||||
if (cachedRateLimits) {
|
if (cachedRateLimits) {
|
||||||
this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from redis cache:
|
/*this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from redis cache:
|
||||||
${cachedRateLimits}
|
${cachedRateLimits}
|
||||||
`);
|
`);*/
|
||||||
return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));
|
return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,15 +166,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
select: { name: true, limit: true, ttl: true, blockDuration: true },
|
select: { name: true, limit: true, ttl: true, blockDuration: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (rateLimits) {
|
|
||||||
this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from database:
|
|
||||||
${JSON.stringify(rateLimits, null, 2)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rateLimits || rateLimits.length === 0) {
|
if (!rateLimits || rateLimits.length === 0) {
|
||||||
rateLimits = [this.getDefaultRateLimit(tracker)];
|
rateLimits = [this.getDefaultRateLimit(tracker)];
|
||||||
this.logger.verbose(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
|
/*this.logger.verbose(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
|
||||||
${JSON.stringify(rateLimits, null, 2)}`);
|
${JSON.stringify(rateLimits, null, 2)}`);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storageService.redis.set(cacheKey, JSON.stringify(rateLimits), "EX", 3600);
|
await this.storageService.redis.set(cacheKey, JSON.stringify(rateLimits), "EX", 3600);
|
||||||
@@ -209,15 +198,15 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
);
|
);
|
||||||
response.setHeader(`X-RateLimit-Reset-${nameFirstUpper}`, timeToBlockExpire || timeToExpire);
|
response.setHeader(`X-RateLimit-Reset-${nameFirstUpper}`, timeToBlockExpire || timeToExpire);
|
||||||
|
|
||||||
this.logger.verbose(
|
/*this.logger.verbose(
|
||||||
`Tracker "${tracker}" rate limit "${name}" incremented. isBlocked ${isBlocked}, totalHits ${totalHits}, timeToExpire ${timeToExpire}, timeToBlockExpire ${timeToBlockExpire}`
|
`Tracker "${tracker}" rate limit "${name}" incremented. isBlocked ${isBlocked}, totalHits ${totalHits}, timeToExpire ${timeToExpire}, timeToBlockExpire ${timeToBlockExpire}`
|
||||||
);
|
);*/
|
||||||
this.logger.verbose(
|
/*this.logger.verbose(
|
||||||
`Tracker "${tracker}" rate limit "${name}" response headers:
|
`Tracker "${tracker}" rate limit "${name}" response headers:
|
||||||
X-RateLimit-Limit-${nameFirstUpper}: ${limit},
|
X-RateLimit-Limit-${nameFirstUpper}: ${limit},
|
||||||
X-RateLimit-Remaining-${nameFirstUpper}: ${timeToBlockExpire ? 0 : Math.max(0, limit - totalHits)},
|
X-RateLimit-Remaining-${nameFirstUpper}: ${timeToBlockExpire ? 0 : Math.max(0, limit - totalHits)},
|
||||||
X-RateLimit-Reset-${nameFirstUpper}: ${timeToBlockExpire || timeToExpire}`
|
X-RateLimit-Reset-${nameFirstUpper}: ${timeToBlockExpire || timeToExpire}`
|
||||||
);
|
);*/
|
||||||
|
|
||||||
return { isBlocked };
|
return { isBlocked };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractUserContext } from "@/lib/extract-user-context";
|
||||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Logger } from "@nestjs/common";
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Logger } from "@nestjs/common";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { tap } from "rxjs/operators";
|
import { tap } from "rxjs/operators";
|
||||||
@@ -14,6 +15,7 @@ export class ResponseInterceptor implements NestInterceptor {
|
|||||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||||
response.setHeader("X-Request-Id", requestId.toString());
|
response.setHeader("X-Request-Id", requestId.toString());
|
||||||
const { method, url } = request;
|
const { method, url } = request;
|
||||||
|
const userContext = extractUserContext(request);
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
@@ -39,6 +41,7 @@ export class ResponseInterceptor implements NestInterceptor {
|
|||||||
responseTime,
|
responseTime,
|
||||||
responseBody: jsonBodyString,
|
responseBody: jsonBodyString,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
...userContext,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ export class EventTypesAtomService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// note (Lauris): don't show platform owner as one of the people that can be assigned to managed team event type
|
// note (Lauris): don't show platform owner as one of the people that can be assigned to managed team event type
|
||||||
const onlyManagedTeamMembers = eventType.teamMembers.filter((user) => user.isPlatformManaged);
|
const onlyManagedTeamMembers = eventType.teamMembers.filter(
|
||||||
|
(user) => user.isPlatformManaged && user.membership === "MEMBER"
|
||||||
|
);
|
||||||
eventType.teamMembers = onlyManagedTeamMembers;
|
eventType.teamMembers = onlyManagedTeamMembers;
|
||||||
|
|
||||||
return eventType;
|
return eventType;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
export type AllowedAuthMethod = "OAUTH_CLIENT_CREDENTIALS" | "API_KEY" | "ACCESS_TOKEN" | "NEXT_AUTH";
|
export type AllowedAuthMethod =
|
||||||
|
| "OAUTH_CLIENT_CREDENTIALS"
|
||||||
|
| "API_KEY"
|
||||||
|
| "ACCESS_TOKEN"
|
||||||
|
| "NEXT_AUTH"
|
||||||
|
| "THIRD_PARTY_ACCESS_TOKEN";
|
||||||
|
|
||||||
export const ApiAuthGuardOnlyAllow = Reflector.createDecorator<AllowedAuthMethod[]>();
|
export const ApiAuthGuardOnlyAllow = Reflector.createDecorator<AllowedAuthMethod[]>();
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { WorkflowsRepository, WorkflowType } from "@/modules/workflows/workflows.repository";
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
NotFoundException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IsWorkflowInTeam implements CanActivate {
|
||||||
|
constructor(private workflowsRepository: WorkflowsRepository) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request & { workflow?: WorkflowType }>();
|
||||||
|
const teamId: string = request.params.teamId;
|
||||||
|
const workflowId: string = request.params.workflowId;
|
||||||
|
|
||||||
|
if (!workflowId) {
|
||||||
|
throw new ForbiddenException("IsWorkflowInTeam - No workflow found in request params.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!teamId) {
|
||||||
|
throw new ForbiddenException("IsWorkflowInTeam - No team id found in request params.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { canAccess, workflow } = await this.checkIfWorkflowIsInTeam(teamId, workflowId);
|
||||||
|
|
||||||
|
if (!canAccess) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
`IsTeamInOrg - Workflow with id=${workflowId} is not part of the team with id=${teamId}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.workflow = workflow;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfWorkflowIsInTeam(
|
||||||
|
teamId: string,
|
||||||
|
workflowId: string
|
||||||
|
): Promise<{ canAccess: boolean; workflow?: WorkflowType }> {
|
||||||
|
const workflow = await this.workflowsRepository.getTeamWorkflowById(Number(teamId), Number(workflowId));
|
||||||
|
|
||||||
|
if (!workflow) {
|
||||||
|
throw new NotFoundException(`IsWorkflowInTeam - workflow (${workflowId}) not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflow.teamId === Number(teamId)) {
|
||||||
|
return { canAccess: true, workflow };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { canAccess: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,16 +10,30 @@ import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
|||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
import {
|
||||||
|
HttpException,
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Logger } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
import * as jwt from "jsonwebtoken";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||||
|
|
||||||
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
|
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
|
||||||
|
|
||||||
|
interface OAuthTokenPayload {
|
||||||
|
userId?: number;
|
||||||
|
teamId?: number;
|
||||||
|
scope: string[];
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
||||||
export type ApiAuthGuardRequest = Request & {
|
export type ApiAuthGuardRequest = Request & {
|
||||||
authMethod: AuthMethods;
|
authMethod: AuthMethods;
|
||||||
@@ -29,8 +43,11 @@ export type ApiAuthGuardRequest = Request & {
|
|||||||
};
|
};
|
||||||
export const NO_AUTH_PROVIDED_MESSAGE =
|
export const NO_AUTH_PROVIDED_MESSAGE =
|
||||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||||
|
private readonly logger = new Logger("ApiAuthStrategy");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly deploymentsService: DeploymentsService,
|
private readonly deploymentsService: DeploymentsService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -54,22 +71,53 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
|
|
||||||
const allowedMethods = request.allowedAuthMethods;
|
const allowedMethods = request.allowedAuthMethods;
|
||||||
const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length;
|
const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length;
|
||||||
|
|
||||||
const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS");
|
const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS");
|
||||||
const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY");
|
const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY");
|
||||||
const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN");
|
const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN");
|
||||||
const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH");
|
const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH");
|
||||||
|
const thirdPartyAccessTokenAllowed =
|
||||||
|
noSpecificAuthExpected || allowedMethods.includes("THIRD_PARTY_ACCESS_TOKEN");
|
||||||
|
|
||||||
if (oAuthClientId && oAuthClientSecret && oAuthAllowed) {
|
if (oAuthClientId && oAuthClientSecret && oAuthAllowed) {
|
||||||
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
||||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bearerToken && (apiKeyAllowed || accessTokenAllowed)) {
|
if (bearerToken) {
|
||||||
const requestOrigin = request.get("Origin");
|
if (!apiKeyAllowed && !accessTokenAllowed && thirdPartyAccessTokenAllowed) {
|
||||||
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
request.authMethod = AuthMethods["THIRD_PARTY_ACCESS_TOKEN"];
|
||||||
? AuthMethods["API_KEY"]
|
const result = await this.validateThirdPartyAccessToken(bearerToken, request);
|
||||||
: AuthMethods["ACCESS_TOKEN"];
|
if (result.success) {
|
||||||
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
return this.success(this.getSuccessUser(result.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKeyAllowed || accessTokenAllowed) {
|
||||||
|
try {
|
||||||
|
const requestOrigin = request.get("Origin");
|
||||||
|
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||||
|
? AuthMethods["API_KEY"]
|
||||||
|
: AuthMethods["ACCESS_TOKEN"];
|
||||||
|
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
||||||
|
} catch (err) {
|
||||||
|
// failed to validate access token, try to validate third party token
|
||||||
|
if (thirdPartyAccessTokenAllowed && request.authMethod === AuthMethods["ACCESS_TOKEN"]) {
|
||||||
|
request.authMethod = AuthMethods["THIRD_PARTY_ACCESS_TOKEN"];
|
||||||
|
const result = await this.validateThirdPartyAccessToken(bearerToken, request);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return this.success(this.getSuccessUser(result.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// token was not third party token, rethrow error from authenticateBearerToken
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return this.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnauthorizedException(`ApiAuthStrategy - Invalid Bearer token`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||||
@@ -83,8 +131,11 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
if (noAuthProvided) {
|
if (noAuthProvided) {
|
||||||
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException(
|
||||||
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${allowedMethods}`
|
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${
|
||||||
|
allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method"
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
@@ -164,21 +215,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
: await this.accessTokenStrategy(authString, request, requestOrigin);
|
: await this.accessTokenStrategy(authString, request, requestOrigin);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return this.error(
|
throw new UnauthorizedException(
|
||||||
new UnauthorizedException(
|
"ApiAuthStrategy - bearer token - No user associated with the provided token"
|
||||||
"ApiAuthStrategy - bearer token - No user associated with the provided token"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.success(this.getSuccessUser(user));
|
return this.success(this.getSuccessUser(user));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
return this.error(err);
|
throw err;
|
||||||
}
|
}
|
||||||
return this.error(
|
throw new InternalServerErrorException("An error occurred while authenticating the request");
|
||||||
new InternalServerErrorException("An error occurred while authenticating the request")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,4 +318,55 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateThirdPartyAccessToken(
|
||||||
|
token: string,
|
||||||
|
request: ApiAuthGuardRequest
|
||||||
|
): Promise<{ success: true; data: UserWithProfile } | { success: false }> {
|
||||||
|
// Removed requiredScopes parameter
|
||||||
|
const encryptionKey = this.config.get<string>("CALENDSO_ENCRYPTION_KEY");
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new InternalServerErrorException("CALENDSO_ENCRYPTION_KEY environment variable is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodedToken: OAuthTokenPayload;
|
||||||
|
try {
|
||||||
|
decodedToken = jwt.verify(token, encryptionKey) as OAuthTokenPayload;
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decodedToken || decodedToken.token_type !== "Access Token") {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: UserWithProfile | null = null;
|
||||||
|
let organizationId: number | null = null;
|
||||||
|
|
||||||
|
if (decodedToken.userId) {
|
||||||
|
user = await this.userRepository.findByIdWithProfile(decodedToken.userId);
|
||||||
|
if (user) {
|
||||||
|
organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||||
|
}
|
||||||
|
} else if (decodedToken.teamId) {
|
||||||
|
const teamOwner = await this.userRepository.findOwnerByTeamIdWithProfile(decodedToken.teamId);
|
||||||
|
if (!teamOwner) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"ApiAuthStrategy - third-party token - No owner found for the associated team."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
user = teamOwner;
|
||||||
|
organizationId =
|
||||||
|
teamOwner.profiles?.find((p) => p.organizationId === decodedToken.teamId)?.organizationId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"ApiAuthStrategy - third-party token - No user or team owner associated with the token."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.organizationId = organizationId;
|
||||||
|
return { success: true, data: user };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/v2/billing/webhook")
|
.post("/v2/billing/webhook")
|
||||||
|
.set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||||
@@ -143,6 +144,7 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
);
|
);
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/v2/billing/webhook")
|
.post("/v2/billing/webhook")
|
||||||
|
.set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||||
@@ -172,6 +174,7 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/v2/billing/webhook")
|
.post("/v2/billing/webhook")
|
||||||
|
.set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||||
@@ -204,6 +207,7 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/v2/billing/webhook")
|
.post("/v2/billing/webhook")
|
||||||
|
.set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
Logger,
|
||||||
Delete,
|
Delete,
|
||||||
|
BadRequestException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { ApiExcludeController } from "@nestjs/swagger";
|
import { ApiExcludeController } from "@nestjs/swagger";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
@@ -116,35 +118,59 @@ export class BillingController {
|
|||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@Headers("stripe-signature") stripeSignature: string
|
@Headers("stripe-signature") stripeSignature: string
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
const event = await this.billingService.stripeService
|
try {
|
||||||
.getStripe()
|
if (!stripeSignature) {
|
||||||
.webhooks.constructEventAsync(request.body, stripeSignature, this.stripeWhSecret);
|
this.logger.warn("Missing stripe-signature header in webhook request");
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.type) {
|
if (!this.stripeWhSecret) {
|
||||||
case "checkout.session.completed":
|
this.logger.error("Missing STRIPE_WEBHOOK_SECRET configuration");
|
||||||
await this.billingService.handleStripeCheckoutEvents(event);
|
return {
|
||||||
break;
|
status: "success",
|
||||||
case "customer.subscription.updated":
|
};
|
||||||
await this.billingService.handleStripePaymentPastDue(event);
|
}
|
||||||
break;
|
|
||||||
case "customer.subscription.deleted":
|
const event = await this.billingService.stripeService
|
||||||
await this.billingService.handleStripeSubscriptionDeleted(event);
|
.getStripe()
|
||||||
break;
|
.webhooks.constructEventAsync(request.body, stripeSignature, this.stripeWhSecret);
|
||||||
case "invoice.created":
|
|
||||||
await this.billingService.handleStripeSubscriptionForActiveManagedUsers(event);
|
switch (event.type) {
|
||||||
break;
|
case "checkout.session.completed":
|
||||||
case "invoice.payment_failed":
|
await this.billingService.handleStripeCheckoutEvents(event);
|
||||||
await this.billingService.handleStripePaymentFailed(event);
|
break;
|
||||||
break;
|
case "customer.subscription.updated":
|
||||||
case "invoice.payment_succeeded":
|
await this.billingService.handleStripePaymentPastDue(event);
|
||||||
await this.billingService.handleStripePaymentSuccess(event);
|
break;
|
||||||
break;
|
case "customer.subscription.deleted":
|
||||||
default:
|
await this.billingService.handleStripeSubscriptionDeleted(event);
|
||||||
break;
|
break;
|
||||||
|
case "invoice.created":
|
||||||
|
await this.billingService.handleStripeSubscriptionForActiveManagedUsers(event);
|
||||||
|
break;
|
||||||
|
case "invoice.payment_failed":
|
||||||
|
await this.billingService.handleStripePaymentFailed(event);
|
||||||
|
break;
|
||||||
|
case "invoice.payment_succeeded":
|
||||||
|
await this.billingService.handleStripePaymentSuccess(event);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Stripe.errors.StripeSignatureVerificationError) {
|
||||||
|
this.logger.error("Webhook signature validation failed", error);
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
status: "success",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BookingSeatRepository {
|
export class BookingSeatRepository {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service";
|
||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
|
import { CalUnifiedCalendarsController } from "@/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller";
|
||||||
|
import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/google-calendar.service";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
GCalService,
|
||||||
|
GoogleCalendarService,
|
||||||
|
AppsRepository,
|
||||||
|
BookingReferencesRepository_2024_08_13,
|
||||||
|
CredentialsRepository,
|
||||||
|
CalendarsService,
|
||||||
|
TokensRepository,
|
||||||
|
SelectedCalendarsRepository,
|
||||||
|
PrismaReadService,
|
||||||
|
PrismaWriteService,
|
||||||
|
UsersRepository,
|
||||||
|
CalendarsRepository,
|
||||||
|
],
|
||||||
|
controllers: [CalUnifiedCalendarsController],
|
||||||
|
exports: [GoogleCalendarService],
|
||||||
|
})
|
||||||
|
export class CalUnifiedCalendarsModule {}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user