Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0922c5ee0 | ||
|
|
f8dc7a115a | ||
|
|
a60aa1bf6c | ||
|
|
bc203d7439 | ||
|
|
d82847035f | ||
|
|
6d133c26f8 | ||
|
|
3ae53a4b6e | ||
|
|
63339d8c01 | ||
|
|
44410c87f0 | ||
|
|
c016241004 | ||
|
|
019ef6de2a | ||
|
|
a2b064b269 | ||
|
|
abb4fd307f | ||
|
|
a9ebb0d160 | ||
|
|
1058c0e171 | ||
|
|
021e5f3833 | ||
|
|
5b5a42e72d | ||
|
|
2b43d9a716 | ||
|
|
3a5325f2a8 | ||
|
|
5ad73607f0 | ||
|
|
5067f0dd16 | ||
|
|
a53e697baa | ||
|
|
e7415fe6e2 | ||
|
|
562309d24b | ||
|
|
3900c55204 | ||
|
|
b3be9a93e0 | ||
|
|
d27ccd6f44 | ||
|
|
f86a5db843 | ||
|
|
4cbf9ec9ab | ||
|
|
aa3c91ebd6 | ||
|
|
071c899ea6 | ||
|
|
32b2e83564 | ||
|
|
7ff6d6559a | ||
|
|
2572a5cb93 | ||
|
|
45e0f87e45 | ||
|
|
6e875fccbe | ||
|
|
689e17bcf6 | ||
|
|
584f9c6e93 | ||
|
|
70348c6f64 | ||
|
|
747591fded | ||
|
|
9bfd933699 | ||
|
|
a775e42e85 | ||
|
|
e3e2bc804a | ||
|
|
b84099aec0 | ||
|
|
073d34f893 | ||
|
|
d72900d164 | ||
|
|
e3b478f5b1 | ||
|
|
a5191d1e5a | ||
|
|
ea21cd5826 | ||
|
|
e78c8738fb | ||
|
|
47314bae83 | ||
|
|
fd8afbc418 | ||
|
|
6e7012757f | ||
|
|
933eb0d057 | ||
|
|
fcb79888f3 | ||
|
|
111e2f30c6 | ||
|
|
f2b3e620b4 | ||
|
|
d9133cb395 | ||
|
|
1722edbd36 | ||
|
|
e68ab3b123 | ||
|
|
cf16cd3ed2 | ||
|
|
f4bc7d2ea4 | ||
|
|
f5f7242f02 | ||
|
|
8f5ac26bb1 | ||
|
|
b983adb16a | ||
|
|
8ce018995a | ||
|
|
f11086e9f8 | ||
|
|
91f5d49cd4 | ||
|
|
55e1e0fa3d | ||
|
|
4a59841b02 | ||
|
|
74a9f3c19e | ||
|
|
89250422ea | ||
|
|
2f2f4dba60 | ||
|
|
b292cafe7b | ||
|
|
82053e0c7c | ||
|
|
860d173f0a | ||
|
|
5bc0e1265d | ||
|
|
e362c37ff3 | ||
|
|
4d53f327d9 | ||
|
|
61f436be2a | ||
|
|
d1a0fc6604 | ||
|
|
976013512c | ||
|
|
d9777a3011 | ||
|
|
7725adedc8 | ||
|
|
7269b3ea60 | ||
|
|
c168df4df0 | ||
|
|
be9b0fb9a8 | ||
|
|
c056cabbbd | ||
|
|
97630e8add | ||
|
|
a5443ccfbe | ||
|
|
6f2986e42e | ||
|
|
db345de99e | ||
|
|
bde40dceb3 | ||
|
|
1a86e7c506 | ||
|
|
9ed1624111 | ||
|
|
5dff1c6df0 | ||
|
|
706d7dff42 | ||
|
|
71443ef23d | ||
|
|
17201334b6 | ||
|
|
bc82ff4d21 | ||
|
|
71ae3f157a | ||
|
|
8784be067f | ||
|
|
84a2e55125 | ||
|
|
21e23f32ac | ||
|
|
33aa9420af | ||
|
|
ef0ba48f49 | ||
|
|
6e54c459a4 | ||
|
|
f5c16f0d6b | ||
|
|
105e7d3e28 | ||
|
|
21ef1900e9 | ||
|
|
f07b73dc79 | ||
|
|
d4b0516235 | ||
|
|
7bfad11ff9 | ||
|
|
d2f7a427d0 | ||
|
|
a910eb96ad | ||
|
|
c080458faa | ||
|
|
5c75cfc17f | ||
|
|
4280f556cb | ||
|
|
21368bb130 | ||
|
|
e9076c8e3e | ||
|
|
2471ee801b | ||
|
+6 |
9fc9876b3f | ||
|
|
34ca3bd22d | ||
|
|
bebe1dd7b4 | ||
|
|
fced9a6355 | ||
|
|
6b57dd6794 | ||
|
|
eb4c58186b | ||
|
|
17e438ab9f | ||
|
|
458e6a3abf | ||
|
|
963d22648b | ||
|
|
fabcf2055b | ||
|
|
633697a812 | ||
|
|
2de33fe1bf | ||
|
|
36e79bf69c | ||
|
|
2e36cadce4 | ||
|
|
9ef532019f | ||
|
|
176dd1a7f3 | ||
|
|
e9c71abd6f | ||
|
|
eb472cba36 | ||
|
|
7d77554696 | ||
|
|
31bfa9e32e | ||
|
|
26be88fc56 | ||
|
|
86cf8feb55 | ||
|
|
1edd255846 | ||
|
|
f538877349 | ||
|
|
d759669c1e | ||
|
|
71ce87ca21 | ||
|
|
7eae004aa2 | ||
|
|
6cd427bc78 | ||
|
|
b8d67c5a41 | ||
|
|
42953548b6 | ||
|
|
28c631e108 | ||
|
|
263a96e660 | ||
|
|
d059b1399b | ||
|
|
12a9c74eb6 | ||
|
|
6bb60e36fd | ||
|
|
c1eb2512b6 | ||
|
|
55d75a9786 | ||
|
|
779eb19e7a | ||
|
|
5d8ed130de | ||
|
|
971d9ff0d8 | ||
|
|
b6bcc43655 | ||
|
|
b576fc8c26 | ||
|
|
821843bc6c | ||
|
|
d19e3e53f0 | ||
|
|
40fee7af51 | ||
|
|
f3462776e8 | ||
|
|
57e9ef7320 | ||
|
|
88f46a6b20 | ||
|
|
17f537774c | ||
|
|
3aa6878c27 | ||
|
|
ae55e5e264 | ||
|
|
36ecece181 | ||
|
|
1a0448910b | ||
|
|
ad5d645347 | ||
|
|
54f7c8328d | ||
|
|
36a1715ece | ||
|
|
edaa6b1de8 | ||
|
|
f47b2066c4 | ||
|
|
7337b3de1e | ||
|
|
933ffcbf69 | ||
|
|
cf50566a10 | ||
|
|
4424aa5b38 | ||
|
|
19634073e9 | ||
|
|
4284e86e51 | ||
|
|
f7c1d62bb8 | ||
|
|
795a228a44 | ||
|
|
0190ca6711 | ||
|
|
ee8cc81862 | ||
|
|
6b244fc5b5 | ||
|
|
356ecfe2fa | ||
|
|
bd410cf09f | ||
|
|
3c5ab5e8d2 | ||
|
|
d938e67e65 | ||
|
|
6a90518231 | ||
|
|
47e8907184 | ||
|
|
d6096f459b | ||
|
|
8839f08257 | ||
|
|
6104add6bd | ||
|
|
7aae55f417 | ||
|
|
ef1b5067c2 | ||
|
|
4caa095d52 | ||
|
|
c8e20b0c57 | ||
|
|
2fb4289798 | ||
|
|
6529b49c3c | ||
|
|
758423ecf9 | ||
|
|
ef11755488 | ||
|
|
03051a2bb1 | ||
|
|
7a57f57e99 | ||
|
|
b6d31129a3 | ||
|
|
34075ee303 | ||
|
|
89f711d929 | ||
|
|
623f0c7d93 | ||
|
|
c67877cc51 | ||
|
|
fd50728169 | ||
|
|
74567909b2 | ||
|
|
d84f08c998 | ||
|
|
685eadc30b | ||
|
|
c821f15a14 | ||
|
|
8a972a74c6 | ||
|
|
cf76cf7aae | ||
|
|
4dd762681c | ||
|
|
399a23a52f | ||
|
|
342f935aba | ||
|
|
27e4210693 | ||
|
|
66cb2bcc13 | ||
|
|
30c31d3585 | ||
|
|
e17303ea88 | ||
|
|
bebf1d80a7 | ||
|
|
5bcefb0b3e | ||
|
|
f52de7331f | ||
|
|
1690770d6a | ||
|
|
7759e6a029 | ||
|
|
0b719ac070 | ||
|
|
baa9045af7 | ||
|
|
b004587262 | ||
|
|
8a7beff339 | ||
|
|
9001eb7795 | ||
|
|
50c4578c2e | ||
|
|
4ff2ce90f9 | ||
|
|
86f8c6c5fa | ||
|
|
0611bbd50e | ||
|
|
b946abab02 | ||
|
|
a091c60618 | ||
|
|
bba7fc1a30 | ||
|
|
864c46736b | ||
|
|
4416709ff6 | ||
|
|
591d42fc8f | ||
|
|
d73b84157a | ||
|
|
680e748b7a | ||
|
|
99b7aaa62d | ||
|
|
3a01e6390f | ||
|
|
a6c25ab7d3 | ||
|
|
df98274a8f | ||
|
|
b540a2ec45 | ||
|
|
8e4459f2fe | ||
|
|
bc5a9f231f | ||
|
|
88984f6175 | ||
|
|
fc592a17ac | ||
|
|
a51524e673 | ||
|
|
cb6553c731 | ||
|
|
101fea2c1c | ||
|
|
aab9193e7b | ||
|
|
a6d2e46b3d | ||
|
|
25ae1493c3 | ||
|
|
56dcab2f6d | ||
|
|
8fbc76d59a | ||
|
|
894c0fc8a7 | ||
|
|
076550b384 | ||
|
|
08e1b0a9c8 | ||
|
|
ef4deb1657 | ||
|
|
f81074c4f5 | ||
|
|
5df14c344e | ||
|
|
cd311f076c | ||
|
|
d60f165223 | ||
|
|
8477128605 | ||
|
|
84606543ae | ||
|
|
a11242e93d | ||
|
|
5939a99aca | ||
|
|
050abd9fab | ||
|
|
1adf2ad5a8 | ||
|
|
3f2d83d89f | ||
|
|
528a4fbb97 | ||
|
|
95e2ad3007 | ||
|
|
c41e5ddc2c | ||
|
|
f206b3d135 | ||
|
|
0b03bcb90e | ||
|
|
b09b32b121 | ||
|
|
d5f7065fdb | ||
|
|
dc40cc664d | ||
|
|
3eaf84e961 | ||
|
|
c36249b5a7 | ||
|
|
3666c260ee | ||
|
|
77f3fd3b38 | ||
|
|
848bcbfb8f | ||
|
|
24936d8b7b | ||
|
|
ad62f9d2bd | ||
|
|
829dd937d6 | ||
|
|
dda4b17a7c | ||
|
|
4def31e98f | ||
|
|
6d3f5495cb | ||
|
|
008af08148 | ||
|
|
ac0930a11f | ||
|
|
0e62800af6 | ||
|
|
b6ab1ef80a | ||
|
|
13347edced | ||
|
|
d734841184 | ||
|
|
a2998b6c84 | ||
|
|
da5df7164a | ||
|
|
3e28160fa7 | ||
|
|
2b9bdb9c80 |
No files matched your search
+26
-1
@@ -334,6 +334,8 @@ AB_TEST_BUCKET_PROBABILITY=50
|
|||||||
# whether we redirect to the future/event-types from event-types or not
|
# whether we redirect to the future/event-types from event-types or not
|
||||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||||
APP_ROUTER_SETTINGS_ADMIN_ENABLED=0
|
APP_ROUTER_SETTINGS_ADMIN_ENABLED=0
|
||||||
|
APP_ROUTER_SETTINGS_PLATFORM_ENABLED=0
|
||||||
|
APP_ROUTER_SETTINGS_ORG_ENABLED=0
|
||||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||||
@@ -341,13 +343,28 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
|||||||
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||||
|
APP_ROUTER_BOOKING_ENABLED=0
|
||||||
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||||
APP_ROUTER_WORKFLOWS_ENABLED=0
|
APP_ROUTER_WORKFLOWS_ENABLED=0
|
||||||
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
|
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
|
||||||
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
||||||
APP_ROUTER_APPS_ENABLED=0
|
APP_ROUTER_APPS_ENABLED=0
|
||||||
APP_ROUTER_VIDEO_ENABLED=0
|
APP_ROUTER_VIDEO_ENABLED=0
|
||||||
|
APP_ROUTER_TEAM_ENABLED=0
|
||||||
APP_ROUTER_TEAMS_ENABLED=0
|
APP_ROUTER_TEAMS_ENABLED=0
|
||||||
|
APP_ROUTER_AVAILABILITY_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_LOGIN_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_NEW_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_SAML_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_ERROR_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_PLATFORM_ENABLED=0
|
||||||
|
APP_ROUTER_AUTH_OAUTH2_ENABLED=0
|
||||||
|
APP_ROUTER_MORE_ENABLED=0
|
||||||
|
APP_ROUTER_MAINTENANCE_ENABLED=0
|
||||||
|
APP_ROUTER_UPGRADE_ENABLED=0
|
||||||
|
APP_ROUTER_CONNECT_AND_JOIN_ENABLED=0
|
||||||
|
|
||||||
# disable setry server source maps
|
# disable setry server source maps
|
||||||
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||||
@@ -362,10 +379,18 @@ TASKER_ENABLE_EMAILS=0
|
|||||||
# Ratelimiting via unkey
|
# Ratelimiting via unkey
|
||||||
UNKEY_ROOT_KEY=
|
UNKEY_ROOT_KEY=
|
||||||
|
|
||||||
|
|
||||||
# Used for Cal.ai Enterprise Voice AI Agents
|
# Used for Cal.ai Enterprise Voice AI Agents
|
||||||
# https://retellai.com
|
# https://retellai.com
|
||||||
RETELL_AI_KEY=
|
RETELL_AI_KEY=
|
||||||
|
|
||||||
# Used to disallow emails as being added as guests on bookings
|
# Used to disallow emails as being added as guests on bookings
|
||||||
BLACKLISTED_GUEST_EMAILS=
|
BLACKLISTED_GUEST_EMAILS=
|
||||||
|
|
||||||
|
# Used to allow browser push notifications
|
||||||
|
# You can use: 'npx web-push generate-vapid-keys' to generate these keys
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY=
|
||||||
|
VAPID_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
|
||||||
|
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
|
||||||
|
NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report any issues with the platform
|
||||||
|
title: ""
|
||||||
|
labels: ["🐛 bug"]
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
Found a bug? Please fill out the sections below. 👍
|
||||||
|
|
||||||
|
### Issue Summary
|
||||||
|
|
||||||
|
A summary of the issue. This needs to be a clear detailed-rich summary.
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
1. (for example) Went to ...
|
||||||
|
2. Clicked on...
|
||||||
|
3. ...
|
||||||
|
|
||||||
|
Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?
|
||||||
|
|
||||||
|
### Actual Results
|
||||||
|
|
||||||
|
- What's happening right now that is different from what is expected
|
||||||
|
|
||||||
|
### Expected Results
|
||||||
|
|
||||||
|
- This is an ideal result that the system should get after the tests are performed
|
||||||
|
|
||||||
|
### Technical details
|
||||||
|
|
||||||
|
- Browser version, screen recording, console logs, network requests: You can make a recording with [Bird Eats Bug](https://birdeatsbug.com/).
|
||||||
|
- Node.js version
|
||||||
|
- Anything else that you think could be an issue.
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
|
||||||
|
- How was this tested? This is quite mandatory in terms of bugs. Providing evidence of your testing with screenshots or/and videos is an amazing way to prove the bug and a troubleshooting chance to find the solution.
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
name: Bug report
|
|
||||||
description: "Please make sure you have checked for any duplicate issues."
|
|
||||||
title: "(bug)"
|
|
||||||
labels: ["🐛 bug"]
|
|
||||||
assignees: []
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: issue-summary
|
|
||||||
attributes:
|
|
||||||
label: Issue Summary
|
|
||||||
description: A summary of the issue. This needs to be a clear detailed-rich summary.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: steps-to-reproduce
|
|
||||||
attributes:
|
|
||||||
label: Steps to Reproduce
|
|
||||||
value: |
|
|
||||||
1. (for example) Went to ...
|
|
||||||
2. Clicked on...
|
|
||||||
3. ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
attributes:
|
|
||||||
label: Expected behavior
|
|
||||||
description: A clear and concise description of what you expected to happen.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: other-information
|
|
||||||
attributes:
|
|
||||||
label: Other information
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: If applicable, add screenshots to help explain your problem.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
id: environment
|
|
||||||
attributes:
|
|
||||||
label: Environment
|
|
||||||
options:
|
|
||||||
- label: app.cal.com
|
|
||||||
- label: Self-hosted Cal
|
|
||||||
- type: textarea
|
|
||||||
id: desktop-version
|
|
||||||
attributes:
|
|
||||||
label: Desktop (please complete the following information)
|
|
||||||
description: |
|
|
||||||
examples:
|
|
||||||
- **OS**: [e.g. iOS]
|
|
||||||
- **Browser**: [e.g. chrome, safari]
|
|
||||||
- **Version**: [e.g. 22]
|
|
||||||
value: |
|
|
||||||
- OS:
|
|
||||||
- Node:
|
|
||||||
- npm:
|
|
||||||
render: markdown
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: markdown
|
|
||||||
id: nodejs-version
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
#### Node.JS version
|
|
||||||
|
|
||||||
[e.g. v18.15.0]
|
|
||||||
- type: markdown
|
|
||||||
id: anything-else
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
#### Anything else?
|
|
||||||
|
|
||||||
- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
|
|
||||||
- Anything else that you think could be an issue?
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
## Mandatory Tasks (DO NOT REMOVE)
|
## Mandatory Tasks (DO NOT REMOVE)
|
||||||
|
|
||||||
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
|
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
|
||||||
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com). If N/A, write N/A here and check the checkbox.
|
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com/docs). If N/A, write N/A here and check the checkbox.
|
||||||
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
|
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ runs:
|
|||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||||
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
|
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
|
||||||
restore-keys: |
|
|
||||||
yarn-download-cache-
|
|
||||||
|
|
||||||
# Invalidated on yarn.lock changes
|
# Invalidated on yarn.lock changes
|
||||||
- name: Restore node_modules
|
- name: Restore node_modules
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ description: "Install playwright, cache and restore if necessary"
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
|
- name: Get installed Playwright version
|
||||||
|
shell: bash
|
||||||
|
id: playwright-version
|
||||||
|
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
uses: buildjet/cache@v4
|
uses: buildjet/cache@v4
|
||||||
@@ -11,9 +15,7 @@ runs:
|
|||||||
~/Library/Caches/ms-playwright
|
~/Library/Caches/ms-playwright
|
||||||
~/.cache/ms-playwright
|
~/.cache/ms-playwright
|
||||||
${{ github.workspace }}/node_modules/playwright
|
${{ github.workspace }}/node_modules/playwright
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||||
restore-keys: cache-playwright-
|
|
||||||
- name: Yarn playwright install
|
- name: Yarn playwright install
|
||||||
shell: bash
|
shell: bash
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
run: yarn playwright install --with-deps
|
||||||
run: yarn playwright install
|
|
||||||
@@ -34,6 +34,11 @@ jobs:
|
|||||||
uses: ./.github/workflows/api-v2-production-build.yml
|
uses: ./.github/workflows/api-v2-production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-atoms:
|
||||||
|
name: Production builds
|
||||||
|
uses: ./.github/workflows/atoms-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: Atoms production Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build atoms
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- name: Cache atoms production build
|
||||||
|
uses: buildjet/cache@v4
|
||||||
|
id: cache-atoms-build
|
||||||
|
env:
|
||||||
|
cache-name: atoms-build
|
||||||
|
key-1: ${{ hashFiles('yarn.lock') }}
|
||||||
|
key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', '!**/node_modules') }}
|
||||||
|
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||||
|
# Ensures production-build.yml will always be fresh
|
||||||
|
key-4: ${{ github.sha }}
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/dist/**
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||||
|
- run: |
|
||||||
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
|
if [ ${{ steps.cache-atoms-build.outputs.cache-hit }} == 'true' ]; then
|
||||||
|
echo "Cache hit for atoms build. Skipping build."
|
||||||
|
else
|
||||||
|
rm -rf packages/platform/atoms/node_modules
|
||||||
|
yarn install
|
||||||
|
yarn workspace @calcom/atoms run build
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: Delete
|
||||||
|
|
||||||
|
on:
|
||||||
|
delete:
|
||||||
|
branches-ignore: [main, gh-pages]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
# ensures that currently running Playwright workflow of deleted branch gets cancelled
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.event.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delete_reports:
|
||||||
|
name: Delete Reports
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# Contains all reports for deleted branch
|
||||||
|
BRANCH_REPORTS_DIR: reports/${{ github.event.ref }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout GitHub Pages Branch
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: calcom/test-results
|
||||||
|
ref: gh-pages
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
- name: Set Git User
|
||||||
|
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Check for workflow reports
|
||||||
|
run: |
|
||||||
|
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
|
||||||
|
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
- name: Delete reports from repo for branch
|
||||||
|
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
|
||||||
|
timeout-minutes: 3
|
||||||
|
run: |
|
||||||
|
cd $BRANCH_REPORTS_DIR/..
|
||||||
|
|
||||||
|
rm -rf ${{ github.event.ref }}
|
||||||
|
git add .
|
||||||
|
git commit -m "workflow: remove all reports for branch ${{ github.event.ref }}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
git pull --rebase
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to rebase. Please review manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Successfully pushed HTML reports to repo."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
name: Crowdin Action
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
synchronize-with-crowdin:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
|
|
||||||
- name: crowdin action
|
|
||||||
uses: crowdin/github-action@v1.13.0
|
|
||||||
with:
|
|
||||||
# upload sources
|
|
||||||
upload_sources: true
|
|
||||||
|
|
||||||
# upload translations (& auto-approve 'em)
|
|
||||||
upload_translations_args: '--auto-approve-imported'
|
|
||||||
upload_translations: true
|
|
||||||
push_translations: true
|
|
||||||
|
|
||||||
# download translations
|
|
||||||
download_translations: true
|
|
||||||
|
|
||||||
# GH config
|
|
||||||
commit_message: "New Crowdin translations by Github Action"
|
|
||||||
localization_branch_name: main
|
|
||||||
create_pull_request: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
||||||
@@ -45,6 +45,9 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
|
credentials:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
options: >-
|
options: >-
|
||||||
|
|||||||
@@ -98,5 +98,5 @@ jobs:
|
|||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-store-results
|
name: blob-report-app-store
|
||||||
path: test-results
|
path: blob-report
|
||||||
@@ -85,5 +85,5 @@ jobs:
|
|||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-react-results
|
name: blob-report-embed-react
|
||||||
path: test-results
|
path: blob-report
|
||||||
@@ -91,5 +91,5 @@ jobs:
|
|||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-core-results
|
name: blob-report-embed-core
|
||||||
path: test-results
|
path: blob-report
|
||||||
@@ -89,8 +89,9 @@ jobs:
|
|||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: blob-report-${{ matrix.shard }}
|
||||||
path: test-results
|
path: blob-report
|
||||||
|
retention-days: 30
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Run i18n AI automation
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-main
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
i18n:
|
||||||
|
name: Run i18n
|
||||||
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: replexica/replexica@main
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.CI_REPLEXICA_API_KEY }}
|
||||||
|
pull-request: true
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-shards
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
jobs:
|
||||||
|
merge-reports:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: all-blob-reports
|
||||||
|
pattern: blob-report-*
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Merge into HTML Report
|
||||||
|
run: yarn playwright merge-reports --reporter html ./all-blob-reports
|
||||||
|
- name: Upload HTML report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||||
|
path: playwright-report
|
||||||
|
retention-days: 14
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
name: PR Reviewed
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label-pr:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Label PR as ready for E2E
|
|
||||||
if: github.event.review.state == 'approved'
|
|
||||||
uses: actions-ecosystem/action-add-labels@v1
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
labels: 'ready-for-e2e'
|
|
||||||
@@ -91,44 +91,57 @@ jobs:
|
|||||||
console.log('Found the label?', labelFound);
|
console.log('Found the label?', labelFound);
|
||||||
core.setOutput('run-e2e', labelFound);
|
core.setOutput('run-e2e', labelFound);
|
||||||
|
|
||||||
|
deps:
|
||||||
|
name: Install dependencies
|
||||||
|
needs: [changes, check-label]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/yarn-install.yml
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
name: Type check
|
name: Type check
|
||||||
needs: [changes, check-label]
|
needs: [changes, check-label, deps]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/check-types.yml
|
uses: ./.github/workflows/check-types.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Linters
|
name: Linters
|
||||||
needs: [changes, check-label]
|
needs: [changes, check-label, deps]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
unit-test:
|
unit-test:
|
||||||
name: Tests
|
name: Tests
|
||||||
needs: [changes, check-label]
|
needs: [changes, check-label, deps]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/unit-tests.yml
|
uses: ./.github/workflows/unit-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build-api-v1:
|
build-api-v1:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
needs: [changes, check-label]
|
needs: [changes, check-label, deps]
|
||||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/api-v1-production-build.yml
|
uses: ./.github/workflows/api-v1-production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build-api-v2:
|
build-api-v2:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
needs: [changes, check-label]
|
needs: [changes, check-label, deps]
|
||||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/api-v2-production-build.yml
|
uses: ./.github/workflows/api-v2-production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-atoms:
|
||||||
|
name: Production builds
|
||||||
|
needs: [changes, check-label, deps]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/atoms-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
needs: [changes, check-label]
|
needs: [changes, check-label, deps]
|
||||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
@@ -181,8 +194,42 @@ jobs:
|
|||||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
merge-reports:
|
||||||
|
name: Merge reports
|
||||||
|
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
|
uses: ./.github/workflows/merge-reports.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
publish-report:
|
||||||
|
name: Publish HTML report
|
||||||
|
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
needs: [changes, check-label, merge-reports]
|
||||||
|
uses: ./.github/workflows/publish-report.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [changes, lint, type-check, unit-test, integration-test, check-label, build, build-api-v1, build-api-v2, e2e, e2e-api-v2, e2e-embed, e2e-embed-react, e2e-app-store]
|
needs:
|
||||||
|
[
|
||||||
|
changes,
|
||||||
|
lint,
|
||||||
|
type-check,
|
||||||
|
unit-test,
|
||||||
|
integration-test,
|
||||||
|
check-label,
|
||||||
|
build,
|
||||||
|
build-api-v1,
|
||||||
|
build-api-v2,
|
||||||
|
e2e,
|
||||||
|
e2e-api-v2,
|
||||||
|
e2e-embed,
|
||||||
|
e2e-embed-react,
|
||||||
|
e2e-app-store,
|
||||||
|
]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
jobs:
|
||||||
|
publish-report:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
# Unique URL path for each workflow run attempt
|
||||||
|
HTML_REPORT_URL_PATH: reports/${{ github.head_ref }}/${{ github.run_id }}/${{ github.run_attempt }}
|
||||||
|
BRANCH_REPORTS_DIR: reports/${{ github.head_ref }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout GitHub Pages Branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: calcom/test-results
|
||||||
|
ref: gh-pages
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
- name: Set Git User
|
||||||
|
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Check for workflow reports
|
||||||
|
run: |
|
||||||
|
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
|
||||||
|
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
- name: Cleanup old HTML reports
|
||||||
|
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
|
||||||
|
timeout-minutes: 3
|
||||||
|
run: |
|
||||||
|
rm -rf $BRANCH_REPORTS_DIR
|
||||||
|
git add .
|
||||||
|
git commit -m "workflow: remove all reports for branch ${{ github.head_ref }}"
|
||||||
|
- name: Download zipped HTML report
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||||
|
path: ${{ env.HTML_REPORT_URL_PATH }}
|
||||||
|
- name: Push HTML Report
|
||||||
|
timeout-minutes: 3
|
||||||
|
# commit report, then try push-rebase-loop until it's able to merge the HTML report to the gh-pages branch
|
||||||
|
# this is necessary when this job running at least twice at the same time (e.g. through two pushes at the same time)
|
||||||
|
run: |
|
||||||
|
git add .
|
||||||
|
git commit -m "workflow: add HTML report for run-id ${{ github.run_id }} (attempt: ${{ github.run_attempt }})"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
git pull --rebase
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to rebase. Please review manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Successfully pushed HTML report to repo."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
- name: Output Report URL as Workflow Annotation
|
||||||
|
id: url
|
||||||
|
run: |
|
||||||
|
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
|
||||||
|
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
||||||
|
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
||||||
|
- name: Find Comment
|
||||||
|
uses: peter-evans/find-comment@v2
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
comment-author: "github-actions[bot]"
|
||||||
|
body-includes: <!-- GENERATED-E2E-RESULTS -->
|
||||||
|
- name: Create comment
|
||||||
|
if: steps.fc.outputs.comment-id == ''
|
||||||
|
uses: peter-evans/create-or-update-comment@v3
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
body: |
|
||||||
|
<!-- GENERATED-E2E-RESULTS -->
|
||||||
|
## E2E results are ready!
|
||||||
|
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||||
|
- name: Update comment
|
||||||
|
if: steps.fc.outputs.comment-id != ''
|
||||||
|
uses: peter-evans/create-or-update-comment@v3
|
||||||
|
with:
|
||||||
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
edit-mode: replace
|
||||||
|
body: |
|
||||||
|
<!-- GENERATED-E2E-RESULTS -->
|
||||||
|
## E2E results are ready!
|
||||||
|
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||||
@@ -15,3 +15,4 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
diff --git a/runtime/binary.js b/runtime/binary.js
|
||||||
|
index c81267d752644043e97b35d26369a5ce266abfd0..034c11262143d1acbf9902745a441d3b0cdfc0fd 100644
|
||||||
|
--- a/runtime/binary.js
|
||||||
|
+++ b/runtime/binary.js
|
||||||
|
@@ -185,7 +185,7 @@ It should have this form: { url: "CONNECTION_STRING" }`);if(r&&typeof r=="object
|
||||||
|
It should have this form: { url: "CONNECTION_STRING" }`);if(typeof i!="string")throw new Me(`Invalid value ${JSON.stringify(i)} for datasource "${t}" provided to PrismaClient constructor.
|
||||||
|
It should have this form: { url: "CONNECTION_STRING" }`)}}}},adapter:(e,A)=>{if(e===null)return;if(e===void 0)throw new Me('"adapter" property must not be undefined, use null to conditionally disable driver adapters.');if(!cg(A).includes("driverAdapters"))throw new Me('"adapter" property can only be provided to PrismaClient constructor when "driverAdapters" preview feature is enabled.')},datasourceUrl:e=>{if(typeof e<"u"&&typeof e!="string")throw new Me(`Invalid value ${JSON.stringify(e)} for "datasourceUrl" provided to PrismaClient constructor.
|
||||||
|
Expected string or undefined.`)},errorFormat:e=>{if(e){if(typeof e!="string")throw new Me(`Invalid value ${JSON.stringify(e)} for "errorFormat" provided to PrismaClient constructor.`);if(!PR.includes(e)){let A=Ni(e,PR);throw new Me(`Invalid errorFormat ${e} provided to PrismaClient constructor.${A}`)}}},log:e=>{if(!e)return;if(!Array.isArray(e))throw new Me(`Invalid value ${JSON.stringify(e)} for "log" provided to PrismaClient constructor.`);function A(t){if(typeof t=="string"&&!GR.includes(t)){let r=Ni(t,GR);throw new Me(`Invalid log level "${t}" provided to PrismaClient constructor.${r}`)}}for(let t of e){A(t);let r={level:A,emit:n=>{let i=["stdout","event"];if(!i.includes(n)){let s=Ni(n,i);throw new Me(`Invalid value ${JSON.stringify(n)} for "emit" in logLevel provided to PrismaClient constructor.${s}`)}}};if(t&&typeof t=="object")for(let[n,i]of Object.entries(t))if(r[n])r[n](i);else throw new Me(`Invalid property ${n} for "log" provided to PrismaClient constructor`)}},__internal:e=>{if(!e)return;let A=["debug","hooks","engine","measurePerformance"];if(typeof e!="object")throw new Me(`Invalid value ${JSON.stringify(e)} for "__internal" to PrismaClient constructor`);for(let[t]of Object.entries(e))if(!A.includes(t)){let r=Ni(t,A);throw new Me(`Invalid property ${JSON.stringify(t)} for "__internal" provided to PrismaClient constructor.${r}`)}}};function YR(e,A){for(let[t,r]of Object.entries(e)){if(!vR.includes(t)){let n=Ni(t,vR);throw new Me(`Unknown property ${t} provided to PrismaClient constructor.${n}`)}VJ[t](r,A)}if(e.datasourceUrl&&e.datasources)throw new Me('Can not use "datasourceUrl" and "datasources" options at the same time. Pick one of them')}function Ni(e,A){if(A.length===0||typeof e!="string")return"";let t=qJ(e,A);return t?` Did you mean "${t}"?`:""}function qJ(e,A){if(A.length===0)return null;let t=A.map(n=>({value:n,distance:(0,JR.default)(e,n)}));t.sort((n,i)=>n.distance<i.distance?-1:1);let r=t[0];return r.distance<3?r.value:null}function VR(e){return e.length===0?Promise.resolve([]):new Promise((A,t)=>{let r=new Array(e.length),n=null,i=!1,s=0,o=()=>{i||(s++,s===e.length&&(i=!0,n?t(n):A(r)))},a=c=>{i||(i=!0,t(c))};for(let c=0;c<e.length;c++)e[c].then(g=>{r[c]=g,o()},g=>{if(!Cg(g)){a(g);return}g.batchRequestIdx===c?a(g):(n||(n=g),o())})})}var pr=ce("prisma:client");typeof globalThis=="object"&&(globalThis.NODE_CLIENT=!0);var OJ={requestArgsToMiddlewareArgs:e=>e,middlewareArgsToRequestArgs:e=>e},HJ=Symbol.for("prisma.client.transaction.id"),WJ={id:0,nextId(){return++this.id}};function _R(e){class A{constructor(r){this._middlewares=new dg;this._createPrismaPromise=ed();this.$extends=Xf;EI(e),r&&YR(r,e);let n=r?.adapter?cf(r.adapter):void 0,i=new HR.EventEmitter().on("error",()=>{});this._extensions=wa.empty(),this._previewFeatures=cg(e),this._clientVersion=e.clientVersion??TR,this._activeProvider=e.activeProvider,this._tracingHelper=DR(this._previewFeatures);let s={rootEnvPath:e.relativeEnvPaths.rootEnvPath&&Qo.default.resolve(e.dirname,e.relativeEnvPaths.rootEnvPath),schemaEnvPath:e.relativeEnvPaths.schemaEnvPath&&Qo.default.resolve(e.dirname,e.relativeEnvPaths.schemaEnvPath)},o=!n&&qi(s,{conflictCheck:"none"})||e.injectableEdgeEnv?.();try{let a=r??{},c=a.__internal??{},g=c.debug===!0;g&&ce.enable("prisma:client");let l=Qo.default.resolve(e.dirname,e.relativePath);WR.default.existsSync(l)||(l=e.dirname),pr("dirname",e.dirname),pr("relativePath",e.relativePath),pr("cwd",l);let u=c.engine||{};if(a.errorFormat?this._errorFormat=a.errorFormat:process.env.NODE_ENV==="production"?this._errorFormat="minimal":process.env.NO_COLOR?this._errorFormat="colorless":this._errorFormat="colorless",this._runtimeDataModel=e.runtimeDataModel,this._engineConfig={cwd:l,dirname:e.dirname,enableDebugLogs:g,allowTriggerPanic:u.allowTriggerPanic,datamodelPath:Qo.default.join(e.dirname,e.filename??"schema.prisma"),prismaPath:u.binaryPath??void 0,engineEndpoint:u.endpoint,generator:e.generator,showColors:this._errorFormat==="pretty",logLevel:a.log&&kR(a.log),logQueries:a.log&&!!(typeof a.log=="string"?a.log==="query":a.log.find(E=>typeof E=="string"?E==="query":E.level==="query")),env:o?.parsed??{},flags:[],clientVersion:e.clientVersion,engineVersion:e.engineVersion,previewFeatures:this._previewFeatures,activeProvider:e.activeProvider,inlineSchema:e.inlineSchema,overrideDatasources:hI(a,e.datasourceNames),inlineDatasources:e.inlineDatasources,inlineSchemaHash:e.inlineSchemaHash,tracingHelper:this._tracingHelper,logEmitter:i,isBundled:e.isBundled,adapter:n},pr("clientVersion",e.clientVersion),this._engine=tR(e,this._engineConfig),this._requestHandler=new Ig(this,i),a.log)for(let E of a.log){let h=typeof E=="string"?E:E.emit==="stdout"?E.level:null;h&&this.$on(h,d=>{Wi.log(`${Wi.tags[h]??""}`,d.message||d.query)})}this._metrics=new yn(this._engine)}catch(a){throw a.clientVersion=this._clientVersion,a}return this._appliedParent=is(this)}get[Symbol.toStringTag](){return"PrismaClient"}$use(r){this._middlewares.use(r)}$o
Line truncated
|
||||||
|
-`)),g?.kind==="batch"&&await g.lock,this._requestHandler.request({protocolQuery:d,modelName:a,action:o,clientMethod:n,dataPath:i,callsite:s,args:r,extensions:this._extensions,transaction:g,unpacker:l,otelParentCtx:u,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:E})}catch(h){throw h.clientVersion=this._clientVersion,h}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new AA("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(r){return!!this._engineConfig.previewFeatures?.includes(r)}}return A}function qR(e,A){return _J(e)?[new QA(e,A),mR]:[e,yR]}function _J(e){return Array.isArray(e)&&Array.isArray(e.raw)}var KJ=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function KR(e){return new Proxy(e,{get(A,t){if(t in A)return A[t];if(!KJ.has(t))throw new TypeError(`Invalid enum value: ${String(t)}`)}})}function jR(e){qi(e,{conflictCheck:"warn"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce});
|
||||||
|
+`)),g?.kind==="batch"&&await g.lock,this._requestHandler.request({protocolQuery:d,modelName:a,action:o,clientMethod:n,dataPath:i,callsite:s,args:r,extensions:this._extensions,transaction:g,unpacker:l,otelParentCtx:u,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:E})}catch(h){throw h.clientVersion=this._clientVersion,h}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new AA("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(r){return!!this._engineConfig.previewFeatures?.includes(r)}}return A}function qR(e,A){return _J(e)?[new QA(e,A),mR]:[e,yR]}function _J(e){return Array.isArray(e)&&Array.isArray(e.raw)}var KJ=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function KR(e){return new Proxy(e,{get(A,t){if(t in A)return A[t];if(!KJ.has(t))throw new TypeError(`Invalid enum value: ${String(t)}`)}})}function jR(e){qi(e,{conflictCheck:"none"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce});
|
||||||
|
/*! Bundled license information:
|
||||||
|
|
||||||
|
undici/lib/fetch/body.js:
|
||||||
|
diff --git a/runtime/library.js b/runtime/library.js
|
||||||
|
index 65b30894c697f97a54924dcf7acc4b7e45002f4d..b2d26124f34759bb222a08e76bcc0f6d52b3c573 100644
|
||||||
|
--- a/runtime/library.js
|
||||||
|
+++ b/runtime/library.js
|
||||||
|
@@ -126,7 +126,7 @@ It should have this form: { url: "CONNECTION_STRING" }`);if(n&&typeof n=="object
|
||||||
|
It should have this form: { url: "CONNECTION_STRING" }`);if(typeof o!="string")throw new q(`Invalid value ${JSON.stringify(o)} for datasource "${r}" provided to PrismaClient constructor.
|
||||||
|
It should have this form: { url: "CONNECTION_STRING" }`)}}}},adapter:(e,t)=>{if(e===null)return;if(e===void 0)throw new q('"adapter" property must not be undefined, use null to conditionally disable driver adapters.');if(!gn(t).includes("driverAdapters"))throw new q('"adapter" property can only be provided to PrismaClient constructor when "driverAdapters" preview feature is enabled.')},datasourceUrl:e=>{if(typeof e<"u"&&typeof e!="string")throw new q(`Invalid value ${JSON.stringify(e)} for "datasourceUrl" provided to PrismaClient constructor.
|
||||||
|
Expected string or undefined.`)},errorFormat:e=>{if(e){if(typeof e!="string")throw new q(`Invalid value ${JSON.stringify(e)} for "errorFormat" provided to PrismaClient constructor.`);if(!Cl.includes(e)){let t=Rt(e,Cl);throw new q(`Invalid errorFormat ${e} provided to PrismaClient constructor.${t}`)}}},log:e=>{if(!e)return;if(!Array.isArray(e))throw new q(`Invalid value ${JSON.stringify(e)} for "log" provided to PrismaClient constructor.`);function t(r){if(typeof r=="string"&&!Al.includes(r)){let n=Rt(r,Al);throw new q(`Invalid log level "${r}" provided to PrismaClient constructor.${n}`)}}for(let r of e){t(r);let n={level:t,emit:i=>{let o=["stdout","event"];if(!o.includes(i)){let s=Rt(i,o);throw new q(`Invalid value ${JSON.stringify(i)} for "emit" in logLevel provided to PrismaClient constructor.${s}`)}}};if(r&&typeof r=="object")for(let[i,o]of Object.entries(r))if(n[i])n[i](o);else throw new q(`Invalid property ${i} for "log" provided to PrismaClient constructor`)}},__internal:e=>{if(!e)return;let t=["debug","hooks","engine","measurePerformance"];if(typeof e!="object")throw new q(`Invalid value ${JSON.stringify(e)} for "__internal" to PrismaClient constructor`);for(let[r]of Object.entries(e))if(!t.includes(r)){let n=Rt(r,t);throw new q(`Invalid property ${JSON.stringify(r)} for "__internal" provided to PrismaClient constructor.${n}`)}}};function Ml(e,t){for(let[r,n]of Object.entries(e)){if(!Tl.includes(r)){let i=Rt(r,Tl);throw new q(`Unknown property ${r} provided to PrismaClient constructor.${i}`)}Xd[r](n,t)}if(e.datasourceUrl&&e.datasources)throw new q('Can not use "datasourceUrl" and "datasources" options at the same time. Pick one of them')}function Rt(e,t){if(t.length===0||typeof e!="string")return"";let r=em(e,t);return r?` Did you mean "${r}"?`:""}function em(e,t){if(t.length===0)return null;let r=t.map(i=>({value:i,distance:(0,Rl.default)(e,i)}));r.sort((i,o)=>i.distance<o.distance?-1:1);let n=r[0];return n.distance<3?n.value:null}function Sl(e){return e.length===0?Promise.resolve([]):new Promise((t,r)=>{let n=new Array(e.length),i=null,o=!1,s=0,a=()=>{o||(s++,s===e.length&&(o=!0,i?r(i):t(n)))},l=u=>{o||(o=!0,r(u))};for(let u=0;u<e.length;u++)e[u].then(c=>{n[u]=c,a()},c=>{if(!Pn(c)){l(c);return}c.batchRequestIdx===u?l(c):(i||(i=c),a())})})}var Ue=D("prisma:client");typeof globalThis=="object"&&(globalThis.NODE_CLIENT=!0);var tm={requestArgsToMiddlewareArgs:e=>e,middlewareArgsToRequestArgs:e=>e},rm=Symbol.for("prisma.client.transaction.id"),nm={id:0,nextId(){return++this.id}};function Dl(e){class t{constructor(n){this._middlewares=new wn;this._createPrismaPromise=Hi();this.$extends=ra;xa(e),n&&Ml(n,e);let i=n?.adapter?fs(n.adapter):void 0,o=new Fl.EventEmitter().on("error",()=>{});this._extensions=ln.empty(),this._previewFeatures=gn(e),this._clientVersion=e.clientVersion??wl,this._activeProvider=e.activeProvider,this._tracingHelper=ml(this._previewFeatures);let s={rootEnvPath:e.relativeEnvPaths.rootEnvPath&&yr.default.resolve(e.dirname,e.relativeEnvPaths.rootEnvPath),schemaEnvPath:e.relativeEnvPaths.schemaEnvPath&&yr.default.resolve(e.dirname,e.relativeEnvPaths.schemaEnvPath)},a=!i&&_t(s,{conflictCheck:"none"})||e.injectableEdgeEnv?.();try{let l=n??{},u=l.__internal??{},c=u.debug===!0;c&&D.enable("prisma:client");let p=yr.default.resolve(e.dirname,e.relativePath);Ol.default.existsSync(p)||(p=e.dirname),Ue("dirname",e.dirname),Ue("relativePath",e.relativePath),Ue("cwd",p);let d=u.engine||{};if(l.errorFormat?this._errorFormat=l.errorFormat:process.env.NODE_ENV==="production"?this._errorFormat="minimal":process.env.NO_COLOR?this._errorFormat="colorless":this._errorFormat="colorless",this._runtimeDataModel=e.runtimeDataModel,this._engineConfig={cwd:p,dirname:e.dirname,enableDebugLogs:c,allowTriggerPanic:d.allowTriggerPanic,datamodelPath:yr.default.join(e.dirname,e.filename??"schema.prisma"),prismaPath:d.binaryPath??void 0,engineEndpoint:d.endpoint,generator:e.generator,showColors:this._errorFormat==="pretty",logLevel:l.log&&gl(l.log),logQueries:l.log&&!!(typeof l.log=="string"?l.log==="query":l.log.find(f=>typeof f=="string"?f==="query":f.level==="query")),env:a?.parsed??{},flags:[],clientVersion:e.clientVersion,engineVersion:e.engineVersion,previewFeatures:this._previewFeatures,activeProvider:e.activeProvider,inlineSchema:e.inlineSchema,overrideDatasources:ba(l,e.datasourceNames),inlineDatasources:e.inlineDatasources,inlineSchemaHash:e.inlineSchemaHash,tracingHelper:this._tracingHelper,logEmitter:o,isBundled:e.isBundled,adapter:i},Ue("clientVersion",e.clientVersion),this._engine=Ua(e,this._engineConfig),this._requestHandler=new Cn(this,o),l.log)for(let f of l.log){let y=typeof f=="string"?f:f.emit==="stdout"?f.level:null;y&&this.$on(y,g=>{$t.log(`${$t.tags[y]??""}`,g.message||g.query)})}this._metrics=new dt(this._engine)}catch(l){throw l.clientVersion=this._clientVersion,l}return this._appliedParent=zt(this)}get[Symbol.toStringTag](){return"PrismaClient"}$use(n){this._middlewares.use(n)}$on(n,i){n==="
Line truncated
|
||||||
|
-`)),c?.kind==="batch"&&await c.lock,this._requestHandler.request({protocolQuery:g,modelName:l,action:a,clientMethod:i,dataPath:o,callsite:s,args:n,extensions:this._extensions,transaction:c,unpacker:p,otelParentCtx:d,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:f})}catch(y){throw y.clientVersion=this._clientVersion,y}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new X("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(n){return!!this._engineConfig.previewFeatures?.includes(n)}}return t}function Il(e,t){return im(e)?[new oe(e,t),ul]:[e,cl]}function im(e){return Array.isArray(e)&&Array.isArray(e.raw)}var om=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function _l(e){return new Proxy(e,{get(t,r){if(r in t)return t[r];if(!om.has(r))throw new TypeError(`Invalid enum value: ${String(r)}`)}})}function Nl(e){_t(e,{conflictCheck:"warn"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce});
|
||||||
|
+`)),c?.kind==="batch"&&await c.lock,this._requestHandler.request({protocolQuery:g,modelName:l,action:a,clientMethod:i,dataPath:o,callsite:s,args:n,extensions:this._extensions,transaction:c,unpacker:p,otelParentCtx:d,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:f})}catch(y){throw y.clientVersion=this._clientVersion,y}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new X("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(n){return!!this._engineConfig.previewFeatures?.includes(n)}}return t}function Il(e,t){return im(e)?[new oe(e,t),ul]:[e,cl]}function im(e){return Array.isArray(e)&&Array.isArray(e.raw)}var om=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function _l(e){return new Proxy(e,{get(t,r){if(r in t)return t[r];if(!om.has(r))throw new TypeError(`Invalid enum value: ${String(r)}`)}})}function Nl(e){_t(e,{conflictCheck:"none"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce});
|
||||||
|
/*! Bundled license information:
|
||||||
|
|
||||||
|
decimal.js/decimal.mjs:
|
||||||
@@ -380,7 +380,7 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
|
|||||||
|
|
||||||
### Elestio
|
### Elestio
|
||||||
|
|
||||||
[](https://elest.io/open-source/cal.com)
|
[](https://elest.io/open-source/cal.com)
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const schemaBookingBaseBodyParams = Booking.pick({
|
|||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true,
|
endTime: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
rescheduledBy: true,
|
||||||
|
cancelledBy: true,
|
||||||
}).partial();
|
}).partial();
|
||||||
|
|
||||||
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
|
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
|
||||||
@@ -33,6 +35,8 @@ const schemaBookingEditParams = z
|
|||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
startTime: iso8601.optional(),
|
startTime: iso8601.optional(),
|
||||||
endTime: iso8601.optional(),
|
endTime: iso8601.optional(),
|
||||||
|
cancelledBy: z.string().email({ message: "Invalid Email" }).optional(),
|
||||||
|
rescheduledBy: z.string().email({ message: "Invalid Email" }).optional(),
|
||||||
// Not supporting responses in edit as that might require re-triggering emails
|
// Not supporting responses in edit as that might require re-triggering emails
|
||||||
// responses
|
// responses
|
||||||
})
|
})
|
||||||
@@ -88,4 +92,6 @@ export const schemaBookingReadPublic = Booking.extend({
|
|||||||
status: true,
|
status: true,
|
||||||
responses: true,
|
responses: true,
|
||||||
fromReschedule: true,
|
fromReschedule: true,
|
||||||
|
cancelledBy: true,
|
||||||
|
rescheduledBy: true,
|
||||||
});
|
});
|
||||||
@@ -168,6 +168,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||||||
metadata: jsonSchema.nullable(),
|
metadata: jsonSchema.nullable(),
|
||||||
customInputs: customInputSchema.array().optional(),
|
customInputs: customInputSchema.array().optional(),
|
||||||
link: z.string().optional(),
|
link: z.string().optional(),
|
||||||
|
hashedLink: z.object({ link: z.string() }).optional().nullable(),
|
||||||
bookingFields: eventTypeBookingFields.optional().nullable(),
|
bookingFields: eventTypeBookingFields.optional().nullable(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
|
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
|
||||||
|
|
||||||
// FIXME: Payment seems a delicate endpoint, do we need to remove anything here?
|
export const schemaPaymentPublic = Payment.pick({
|
||||||
export const schemaPaymentBodyParams = Payment.omit({ id: true });
|
id: true,
|
||||||
export const schemaPaymentPublic = Payment.omit({ externalId: true });
|
amount: true,
|
||||||
|
success: true,
|
||||||
|
refunded: true,
|
||||||
|
fee: true,
|
||||||
|
paymentOption: true,
|
||||||
|
currency: true,
|
||||||
|
bookingId: true,
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
|
|||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
isPlatform: true,
|
isPlatform: true,
|
||||||
smsLockState: true,
|
smsLockState: true,
|
||||||
|
smsLockReviewedByAdmin: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const schemaTeamRequiredParams = z.object({
|
const schemaTeamRequiredParams = z.object({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ enum locales {
|
|||||||
RO = "ro",
|
RO = "ro",
|
||||||
NL = "nl",
|
NL = "nl",
|
||||||
PT_BR = "pt-BR",
|
PT_BR = "pt-BR",
|
||||||
// ES_419 = "es-419", // Disabled until Crowdin reaches at least 80% completion
|
ES_419 = "es-419",
|
||||||
KO = "ko",
|
KO = "ko",
|
||||||
JA = "ja",
|
JA = "ja",
|
||||||
PL = "pl",
|
PL = "pl",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* tags:
|
* tags:
|
||||||
* - availabilities
|
* - availabilities
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/availability
|
* url: https://docs.cal.com/docs/core-features/availability
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, availability removed successfully
|
* description: OK, availability removed successfully
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* tags:
|
* tags:
|
||||||
* - availabilities
|
* - availabilities
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/availability
|
* url: https://docs.cal.com/docs/core-features/availability
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK
|
* description: OK
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* tags:
|
* tags:
|
||||||
* - availabilities
|
* - availabilities
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/availability
|
* url: https://docs.cal.com/docs/core-features/availability
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, availability edited successfully
|
* description: OK, availability edited successfully
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import {
|
|||||||
* tags:
|
* tags:
|
||||||
* - availabilities
|
* - availabilities
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/availability
|
* url: https://docs.cal.com/docs/core-features/availability
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, availability created
|
* description: OK, availability created
|
||||||
|
|||||||
@@ -30,40 +30,59 @@ async function authMiddleware(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
include: {
|
select: {
|
||||||
|
email: true,
|
||||||
bookings: true,
|
bookings: true,
|
||||||
teams: {
|
},
|
||||||
select: {
|
});
|
||||||
teamId: true,
|
|
||||||
|
if (!user) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||||
|
|
||||||
|
const filteredBookings = user?.bookings?.filter((booking) => booking.id === id);
|
||||||
|
const userIsHost = !!filteredBookings?.length;
|
||||||
|
|
||||||
|
const bookingsAsAttendee = prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
attendees: { some: { email: user.email } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookingsAsEventTypeOwner = prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
eventType: {
|
||||||
|
owner: { id: userId },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookingsAsTeamOwnerOrAdmin = prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
eventType: {
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: { userId, role: { in: ["ADMIN", "OWNER"] }, accepted: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
const [resultOne, resultTwo, resultThree] = await Promise.all([
|
||||||
|
bookingsAsAttendee,
|
||||||
|
bookingsAsEventTypeOwner,
|
||||||
|
bookingsAsTeamOwnerOrAdmin,
|
||||||
|
]);
|
||||||
|
|
||||||
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
const teamBookingsAsOwnerOrAdmin = [...resultOne, ...resultTwo, ...resultThree];
|
||||||
|
const userHasTeamBookings = !!teamBookingsAsOwnerOrAdmin.length;
|
||||||
|
|
||||||
if (!userBookingIds.includes(id)) {
|
if (!userIsHost && !userHasTeamBookings)
|
||||||
const teamBookings = await prisma.booking.findUnique({
|
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
eventType: {
|
|
||||||
team: {
|
|
||||||
id: {
|
|
||||||
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!teamBookings) {
|
|
||||||
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default authMiddleware;
|
export default authMiddleware;
|
||||||
@@ -11,8 +11,8 @@ const swaggerHandler = withSwagger({
|
|||||||
{ url: "https://api.cal.com/v1" },
|
{ url: "https://api.cal.com/v1" },
|
||||||
],
|
],
|
||||||
externalDocs: {
|
externalDocs: {
|
||||||
url: "https://docs.cal.com",
|
url: "https://docs.cal.com/docs",
|
||||||
description: "Find more info at our main docs: https://docs.cal.com/",
|
description: "Find more info at our main docs: https://docs.cal.com/docs/",
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
title: `${pjson.name}: ${pjson.description}`,
|
title: `${pjson.name}: ${pjson.description}`,
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import prisma from "@calcom/prisma";
|
|||||||
|
|
||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
|
import checkParentEventOwnership from "../_utils/checkParentEventOwnership";
|
||||||
|
import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /event-types/{id}:
|
* /event-types/{id}:
|
||||||
@@ -28,7 +31,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* tags:
|
* tags:
|
||||||
* - event-types
|
* - event-types
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/event-types
|
* url: https://docs.cal.com/docs/core-features/event-types
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, eventType removed successfully
|
* description: OK, eventType removed successfully
|
||||||
@@ -49,9 +52,18 @@ async function checkPermissions(req: NextApiRequest) {
|
|||||||
const { userId, isSystemWideAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||||
if (isSystemWideAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
/** Only event type owners can delete it */
|
|
||||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
const eventType = await prisma.eventType.findFirst({ where: { id } });
|
||||||
|
|
||||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|
||||||
|
/** Only event type owners or team owners for team events can delete it */
|
||||||
|
if (eventType.teamId) return await checkTeamEventEditPermission(req, { teamId: eventType.teamId });
|
||||||
|
|
||||||
|
if (eventType.parentId) return await checkParentEventOwnership(req);
|
||||||
|
|
||||||
|
if (eventType.userId && eventType.userId !== userId)
|
||||||
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(deleteHandler);
|
export default defaultResponder(deleteHandler);
|
||||||
@@ -34,7 +34,7 @@ import getCalLink from "../_utils/getCalLink";
|
|||||||
* tags:
|
* tags:
|
||||||
* - event-types
|
* - event-types
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/event-types
|
* url: https://docs.cal.com/docs/core-features/event-types
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK
|
* description: OK
|
||||||
@@ -51,6 +51,7 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
|
hashedLink: { select: { link: true } },
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
hosts: { select: { userId: true, isFixed: true } },
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
|||||||
* tags:
|
* tags:
|
||||||
* - event-types
|
* - event-types
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/event-types
|
* url: https://docs.cal.com/docs/core-features/event-types
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, eventType edited successfully
|
* description: OK, eventType edited successfully
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import getCalLink from "./_utils/getCalLink";
|
|||||||
* tags:
|
* tags:
|
||||||
* - event-types
|
* - event-types
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/event-types
|
* url: https://docs.cal.com/docs/core-features/event-types
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK
|
* description: OK
|
||||||
@@ -56,6 +56,7 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
|
hashedLink: { select: { link: true } },
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
hosts: { select: { userId: true, isFixed: true } },
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
|||||||
* tags:
|
* tags:
|
||||||
* - event-types
|
* - event-types
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/event-types
|
* url: https://docs.cal.com/docs/core-features/event-types
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, event type created
|
* description: OK, event type created
|
||||||
|
|||||||
@@ -18,12 +18,8 @@ export default async function checkParentEventOwnership(req: NextApiRequest) {
|
|||||||
/** These are already parsed upstream, we can assume they're good here. */
|
/** These are already parsed upstream, we can assume they're good here. */
|
||||||
const parentId = Number(body.parentId);
|
const parentId = Number(body.parentId);
|
||||||
const parentEventType = await prisma.eventType.findUnique({
|
const parentEventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: { id: parentId },
|
||||||
id: parentId,
|
select: { teamId: true },
|
||||||
},
|
|
||||||
select: {
|
|
||||||
teamId: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!parentEventType) {
|
if (!parentEventType) {
|
||||||
@@ -44,7 +40,8 @@ export default async function checkParentEventOwnership(req: NextApiRequest) {
|
|||||||
where: {
|
where: {
|
||||||
teamId: parentEventType.teamId,
|
teamId: parentEventType.teamId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
OR: [{ role: "OWNER" }, { role: "ADMIN" }],
|
role: { in: ["ADMIN", "OWNER"] },
|
||||||
|
accepted: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,21 +10,17 @@ export default async function checkTeamEventEditPermission(
|
|||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
|
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
|
||||||
) {
|
) {
|
||||||
const { isSystemWideAdmin } = req;
|
|
||||||
let userId = req.userId;
|
|
||||||
if (isSystemWideAdmin && body.userId) {
|
|
||||||
userId = body.userId;
|
|
||||||
}
|
|
||||||
if (body.teamId) {
|
if (body.teamId) {
|
||||||
const membership = await prisma.membership.findFirst({
|
const membership = await prisma.membership.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId: req.userId,
|
||||||
teamId: body.teamId,
|
teamId: body.teamId,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
|
role: { in: ["ADMIN", "OWNER"] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
|
if (!membership) {
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: "No permission to operate on event-type for this team",
|
message: "No permission to operate on event-type for this team",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import utc from "dayjs/plugin/utc";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
|
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
@@ -17,14 +18,22 @@ import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
|||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
let isColdStart = true;
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const { usernameList, ...rest } = req.query;
|
if (isColdStart && IS_PRODUCTION) {
|
||||||
|
console.log("Cold start of /v1/slots detected");
|
||||||
|
isColdStart = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { usernameList, isTeamEvent, ...rest } = req.query;
|
||||||
|
const parsedIsTeamEvent = String(isTeamEvent).toLowerCase() === "true";
|
||||||
let slugs = usernameList;
|
let slugs = usernameList;
|
||||||
if (!Array.isArray(usernameList)) {
|
if (!Array.isArray(usernameList)) {
|
||||||
slugs = usernameList ? [usernameList] : undefined;
|
slugs = usernameList ? [usernameList] : undefined;
|
||||||
}
|
}
|
||||||
const input = getScheduleSchema.parse({ usernameList: slugs, ...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 availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||||
const slotsInProvidedTimeZone = timeZoneSupported
|
const slotsInProvidedTimeZone = timeZoneSupported
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export async function canUserAccessTeamWithRole(
|
|||||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||||
if (!isSystemWideAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
if (!isSystemWideAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||||
const team = await prisma.team.findFirst(args);
|
const team = await prisma.team.findFirst(args);
|
||||||
if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: ${role.toString()} required` });
|
if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: OWNER or ADMIN role required` });
|
||||||
return team;
|
return team;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||||
} = {
|
} = {
|
||||||
...data,
|
...data,
|
||||||
|
smsLockReviewedByAdmin: false,
|
||||||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||||
};
|
};
|
||||||
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const querySchema = z.object({
|
|||||||
* tags:
|
* tags:
|
||||||
* - event-types
|
* - event-types
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/event-types
|
* url: https://docs.cal.com/docs/core-features/event-types
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK
|
* description: OK
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||||
} = {
|
} = {
|
||||||
...data,
|
...data,
|
||||||
|
smsLockReviewedByAdmin: false,
|
||||||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,6 +215,7 @@ const generateTeamCheckoutSession = async ({
|
|||||||
metadata: {
|
metadata: {
|
||||||
pendingPaymentTeamId,
|
pendingPaymentTeamId,
|
||||||
ownerId,
|
ownerId,
|
||||||
|
dubCustomerId: ownerId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
|||||||
* tags:
|
* tags:
|
||||||
* - webhooks
|
* - webhooks
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/webhooks
|
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, hook removed successfully
|
* description: OK, hook removed successfully
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
|
|||||||
* tags:
|
* tags:
|
||||||
* - webhooks
|
* - webhooks
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/webhooks
|
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK
|
* description: OK
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
|||||||
* tags:
|
* tags:
|
||||||
* - webhooks
|
* - webhooks
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/webhooks
|
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, webhook edited successfully
|
* description: OK, webhook edited successfully
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
|
|||||||
* tags:
|
* tags:
|
||||||
* - webhooks
|
* - webhooks
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/webhooks
|
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK
|
* description: OK
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
|
|||||||
* tags:
|
* tags:
|
||||||
* - webhooks
|
* - webhooks
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
* url: https://docs.cal.com/core-features/webhooks
|
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||||
* responses:
|
* responses:
|
||||||
* 201:
|
* 201:
|
||||||
* description: OK, webhook created
|
* description: OK, webhook created
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# Set the version of docker compose to use
|
|
||||||
version: "3.9"
|
|
||||||
|
|
||||||
# The containers that compose the project
|
# The containers that compose the project
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
|
|||||||
@@ -1,90 +1,332 @@
|
|||||||
|
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect, test } from "vitest";
|
||||||
|
|
||||||
import type { HttpError } from "@calcom/lib/http-error";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import authMiddleware from "../../../pages/api/bookings/[id]/_auth-middleware";
|
import authMiddleware from "../../../pages/api/bookings/[id]/_auth-middleware";
|
||||||
|
|
||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
describe("Bookings auth middleware", () => {
|
describe("Booking ownership and access in Middleware", () => {
|
||||||
it("Returns 403 when user has no permission to the booking", async () => {
|
const adminUserId = 1;
|
||||||
const trialUser = await prisma.user.findFirstOrThrow({ where: { email: "trial@example.com" } });
|
const ownerUserId = 2;
|
||||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
const memberUserId = 3;
|
||||||
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
const orgOwnerUserId = 4;
|
||||||
|
const adminUserEmail = "admin@example.com";
|
||||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const ownerUserEmail = "owner@example.com";
|
||||||
method: "GET",
|
const memberUserEmail = "member@example.com";
|
||||||
body: {},
|
const orgOwnerUserEmail = "org-owner@example.com";
|
||||||
query: {
|
// mock user data
|
||||||
id: booking.id,
|
function buildMockData() {
|
||||||
|
//Create Users
|
||||||
|
prismock.user.create({
|
||||||
|
data: {
|
||||||
|
id: adminUserId,
|
||||||
|
username: "admin",
|
||||||
|
name: "Admin User",
|
||||||
|
email: adminUserEmail,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
prismock.user.create({
|
||||||
|
data: {
|
||||||
|
id: ownerUserId,
|
||||||
|
username: "owner",
|
||||||
|
name: "Owner User",
|
||||||
|
email: ownerUserEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
prismock.user.create({
|
||||||
|
data: {
|
||||||
|
id: orgOwnerUserId,
|
||||||
|
username: "org-owner",
|
||||||
|
name: "Org Owner",
|
||||||
|
email: orgOwnerUserEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
prismock.user.create({
|
||||||
|
data: {
|
||||||
|
id: memberUserId,
|
||||||
|
username: "member",
|
||||||
|
name: "Member User",
|
||||||
|
email: memberUserEmail,
|
||||||
|
bookings: {
|
||||||
|
create: {
|
||||||
|
id: 2,
|
||||||
|
uid: "2",
|
||||||
|
title: "Booking 2",
|
||||||
|
eventTypeId: 1,
|
||||||
|
startTime: "2024-08-30T06:45:00.000Z",
|
||||||
|
endTime: "2024-08-30T07:45:00.000Z",
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
name: "Member User",
|
||||||
|
email: memberUserEmail,
|
||||||
|
timeZone: "UTC",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//create team
|
||||||
|
prismock.team.create({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
name: "Team 1",
|
||||||
|
slug: "team1",
|
||||||
|
members: {
|
||||||
|
createMany: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
userId: adminUserId,
|
||||||
|
role: MembershipRole.ADMIN,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: ownerUserId,
|
||||||
|
role: MembershipRole.OWNER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: memberUserId,
|
||||||
|
role: MembershipRole.MEMBER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//create Org
|
||||||
|
prismock.team.create({
|
||||||
|
data: {
|
||||||
|
id: 2,
|
||||||
|
name: "Org",
|
||||||
|
slug: "org",
|
||||||
|
isOrganization: true,
|
||||||
|
children: {
|
||||||
|
connect: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
createMany: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
userId: orgOwnerUserId,
|
||||||
|
role: MembershipRole.OWNER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: memberUserId,
|
||||||
|
role: MembershipRole.MEMBER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: ownerUserId,
|
||||||
|
role: MembershipRole.MEMBER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: adminUserId,
|
||||||
|
role: MembershipRole.MEMBER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//create eventTypes
|
||||||
|
prismock.eventType.create({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
title: "Event 1",
|
||||||
|
slug: "event",
|
||||||
|
length: 60,
|
||||||
|
bookings: {
|
||||||
|
connect: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
prismock.eventType.create({
|
||||||
|
data: {
|
||||||
|
id: 2,
|
||||||
|
title: "Event 2",
|
||||||
|
slug: "event",
|
||||||
|
length: 60,
|
||||||
|
teamId: 1,
|
||||||
|
bookings: {
|
||||||
|
connect: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//link eventType to teams
|
||||||
|
prismock.eventType.update({
|
||||||
|
where: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
owner: {
|
||||||
|
connect: {
|
||||||
|
id: ownerUserId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
prismock.eventType.update({
|
||||||
|
where: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
team: {
|
||||||
|
connect: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//link team to org
|
||||||
|
prismock.team.update({
|
||||||
|
where: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
parentId: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Call Prisma to create booking with attendees
|
||||||
|
prismock.booking.create({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
uid: "1",
|
||||||
|
title: "Booking 1",
|
||||||
|
userId: 1,
|
||||||
|
startTime: "2024-08-30T06:45:00.000Z",
|
||||||
|
endTime: "2024-08-30T07:45:00.000Z",
|
||||||
|
eventTypeId: 2,
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
name: "Admin User",
|
||||||
|
email: adminUserEmail,
|
||||||
|
timeZone: "UTC",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
req.userId = trialUser.id;
|
test("should not throw error for bookings where user is an attendee", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
try {
|
method: "GET",
|
||||||
await authMiddleware(req);
|
query: {
|
||||||
} catch (error) {
|
id: 2,
|
||||||
const httpError = error as HttpError;
|
},
|
||||||
expect(httpError.statusCode).toBe(403);
|
prisma: prismock,
|
||||||
}
|
});
|
||||||
|
buildMockData();
|
||||||
|
req.userId = memberUserId;
|
||||||
|
await expect(authMiddleware(req)).resolves.not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("No error is thrown when user is the booking user", async () => {
|
test("should throw error for bookings where user is not an attendee", async () => {
|
||||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
|
||||||
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
|
||||||
|
|
||||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
body: {},
|
|
||||||
query: {
|
query: {
|
||||||
id: booking.id,
|
id: 1,
|
||||||
},
|
},
|
||||||
|
prisma: prismock,
|
||||||
});
|
});
|
||||||
|
buildMockData();
|
||||||
|
req.userId = memberUserId;
|
||||||
|
|
||||||
req.userId = proUser.id;
|
await expect(authMiddleware(req)).rejects.toThrow();
|
||||||
|
|
||||||
await authMiddleware(req);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("No error is thrown when user is system-wide admin", async () => {
|
test("should not throw error for booking where user is the event type owner", async () => {
|
||||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
|
|
||||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
|
||||||
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
|
||||||
|
|
||||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
body: {},
|
|
||||||
query: {
|
query: {
|
||||||
id: booking.id,
|
id: 2,
|
||||||
},
|
},
|
||||||
|
prisma: prismock,
|
||||||
});
|
});
|
||||||
|
buildMockData();
|
||||||
|
req.userId = ownerUserId;
|
||||||
|
await expect(authMiddleware(req)).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
req.userId = adminUser.id;
|
test("should not throw error for booking where user is team owner or admin", async () => {
|
||||||
|
const { req: req1 } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
prisma: prismock,
|
||||||
|
});
|
||||||
|
const { req: req2 } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
prisma: prismock,
|
||||||
|
});
|
||||||
|
buildMockData();
|
||||||
|
|
||||||
|
req1.userId = adminUserId;
|
||||||
|
req2.userId = ownerUserId;
|
||||||
|
|
||||||
|
await expect(authMiddleware(req1)).resolves.not.toThrow();
|
||||||
|
await expect(authMiddleware(req2)).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
test("should throw error for booking where user is not team owner or admin", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
prisma: prismock,
|
||||||
|
});
|
||||||
|
buildMockData();
|
||||||
|
|
||||||
|
req.userId = memberUserId;
|
||||||
|
|
||||||
|
await expect(authMiddleware(req)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
test("should not throw error when user is system-wide admin", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
prisma: prismock,
|
||||||
|
});
|
||||||
|
buildMockData();
|
||||||
|
req.userId = adminUserId;
|
||||||
req.isSystemWideAdmin = true;
|
req.isSystemWideAdmin = true;
|
||||||
|
|
||||||
await authMiddleware(req);
|
await authMiddleware(req);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("No error is thrown when user is org-wide admin", async () => {
|
it("should throw error when user is org-wide admin", async () => {
|
||||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
|
||||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member1-acme@example.com" } });
|
|
||||||
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: memberUser.id } });
|
|
||||||
|
|
||||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
body: {},
|
|
||||||
query: {
|
query: {
|
||||||
id: booking.id,
|
id: 1,
|
||||||
},
|
},
|
||||||
|
prisma: prismock,
|
||||||
});
|
});
|
||||||
|
buildMockData();
|
||||||
req.userId = adminUser.id;
|
req.userId = orgOwnerUserId;
|
||||||
req.isOrganizationOwnerOrAdmin = true;
|
req.isOrganizationOwnerOrAdmin = true;
|
||||||
|
|
||||||
await authMiddleware(req);
|
await authMiddleware(req);
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
|
import { buildEventType } from "@calcom/lib/test/builder";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import handler from "../../../../pages/api/event-types/[id]/_delete";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
describe("DELETE /api/event-types/[id]", () => {
|
||||||
|
const eventTypeId = 1234567;
|
||||||
|
const teamId = 9999;
|
||||||
|
const adminUser = 1111;
|
||||||
|
const memberUser = 2222;
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
// Mocking membership.findFirst
|
||||||
|
prismaMock.membership.findFirst.mockImplementation(({ where }) => {
|
||||||
|
const { userId, teamId, accepted, role } = where;
|
||||||
|
const mockData = [
|
||||||
|
{ userId: 1111, teamId: teamId, accepted: true, role: MembershipRole.ADMIN },
|
||||||
|
{ userId: 2222, teamId: teamId, accepted: true, role: MembershipRole.MEMBER },
|
||||||
|
];
|
||||||
|
// Return the correct user based on the query conditions
|
||||||
|
return mockData.find(
|
||||||
|
(membership) =>
|
||||||
|
membership.userId === userId &&
|
||||||
|
membership.teamId === teamId &&
|
||||||
|
membership.accepted === accepted &&
|
||||||
|
role.in.includes(membership.role)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mocking eventType.findFirst
|
||||||
|
prismaMock.eventType.findFirst.mockResolvedValue(
|
||||||
|
buildEventType({
|
||||||
|
id: eventTypeId,
|
||||||
|
teamId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mocking team.findUnique
|
||||||
|
prismaMock.team.findUnique.mockResolvedValue({
|
||||||
|
id: teamId,
|
||||||
|
members: [
|
||||||
|
{ userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId },
|
||||||
|
{ userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error", async () => {
|
||||||
|
test("Fails to remove event type if user is not OWNER/ADMIN of team associated with event type", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign userId to the request objects
|
||||||
|
req.userId = memberUser;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(403); // Check if the deletion was successful
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Success", async () => {
|
||||||
|
test("Removes event type if user is owner of team associated with event type", async () => {
|
||||||
|
// Mocks for DELETE request
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign userId to the request objects
|
||||||
|
req.userId = adminUser;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(200); // Check if the deletion was successful
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
|
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
|
||||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
|
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build ",
|
||||||
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
||||||
|
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
||||||
"test": "yarn dev:build && jest",
|
"test": "yarn dev:build && jest",
|
||||||
@@ -19,14 +20,16 @@
|
|||||||
"test:cov": "yarn dev:build && jest --coverage",
|
"test:cov": "yarn dev:build && jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json",
|
"test:e2e": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json",
|
||||||
|
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
|
||||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
|
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
|
"@calcom/platform-enums": "*",
|
||||||
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.36",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||||
"@calcom/platform-libraries-0.0.23": "npm:@calcom/platform-libraries@0.0.23",
|
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
||||||
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
import { BillingModule } from "@/modules/billing/billing.module";
|
import { BillingModule } from "@/modules/billing/billing.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -10,7 +11,7 @@ import { Module } from "@nestjs/common";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
|
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
|
||||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
|
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeyRepository],
|
||||||
controllers: [BookingsController],
|
controllers: [BookingsController],
|
||||||
})
|
})
|
||||||
export class BookingsModule {}
|
export class BookingsModule {}
|
||||||
@@ -14,14 +14,15 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { handleNewBooking } from "@calcom/platform-libraries-0.0.23";
|
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
import { ApiSuccessResponse, ApiResponse, ApiErrorResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Bookings Endpoints", () => {
|
describe("Bookings Endpoints", () => {
|
||||||
describe("User Authenticated", () => {
|
describe("User Authenticated", () => {
|
||||||
@@ -31,6 +32,8 @@ describe("Bookings Endpoints", () => {
|
|||||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
let schedulesService: SchedulesService_2024_04_15;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let apiKeyString: string;
|
||||||
|
|
||||||
const userEmail = "bookings-controller-e2e@api.com";
|
const userEmail = "bookings-controller-e2e@api.com";
|
||||||
let user: User;
|
let user: User;
|
||||||
@@ -56,11 +59,13 @@ describe("Bookings Endpoints", () => {
|
|||||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||||
|
apiKeyString = keyString;
|
||||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
name: "working time",
|
name: "working time",
|
||||||
timeZone: "Europe/Rome",
|
timeZone: "Europe/Rome",
|
||||||
@@ -139,6 +144,105 @@ describe("Bookings Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should fail to create a booking with no_available_users_found_error", async () => {
|
||||||
|
const bookingStart = "2040-05-21T09:30:00.000Z";
|
||||||
|
const bookingEnd = "2040-05-21T10:30:00.000Z";
|
||||||
|
const bookingEventTypeId = eventTypeId;
|
||||||
|
const bookingTimeZone = "Europe/London";
|
||||||
|
const bookingLanguage = "en";
|
||||||
|
const bookingHashedLink = "";
|
||||||
|
const bookingMetadata = {
|
||||||
|
timeFormat: "12",
|
||||||
|
meetingType: "organizer-phone",
|
||||||
|
};
|
||||||
|
const bookingResponses = {
|
||||||
|
name: "tester",
|
||||||
|
email: "tester@example.com",
|
||||||
|
location: {
|
||||||
|
value: "link",
|
||||||
|
optionValue: "",
|
||||||
|
},
|
||||||
|
notes: "test",
|
||||||
|
guests: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const body: CreateBookingInput = {
|
||||||
|
start: bookingStart,
|
||||||
|
end: bookingEnd,
|
||||||
|
eventTypeId: bookingEventTypeId,
|
||||||
|
timeZone: bookingTimeZone,
|
||||||
|
language: bookingLanguage,
|
||||||
|
metadata: bookingMetadata,
|
||||||
|
hashedLink: bookingHashedLink,
|
||||||
|
responses: bookingResponses,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.expect(400)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiErrorResponse = response.body;
|
||||||
|
expect(responseBody.error.message).toEqual("no_available_users_found_error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a booking with api key to get owner id", async () => {
|
||||||
|
const bookingStart = "2040-05-22T09:30:00.000Z";
|
||||||
|
const bookingEnd = "2040-05-22T10:30:00.000Z";
|
||||||
|
const bookingEventTypeId = eventTypeId;
|
||||||
|
const bookingTimeZone = "Europe/London";
|
||||||
|
const bookingLanguage = "en";
|
||||||
|
const bookingHashedLink = "";
|
||||||
|
const bookingMetadata = {
|
||||||
|
timeFormat: "12",
|
||||||
|
meetingType: "organizer-phone",
|
||||||
|
};
|
||||||
|
const bookingResponses = {
|
||||||
|
name: "tester",
|
||||||
|
email: "tester@example.com",
|
||||||
|
location: {
|
||||||
|
value: "link",
|
||||||
|
optionValue: "",
|
||||||
|
},
|
||||||
|
notes: "test",
|
||||||
|
guests: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const body: CreateBookingInput = {
|
||||||
|
start: bookingStart,
|
||||||
|
end: bookingEnd,
|
||||||
|
eventTypeId: bookingEventTypeId,
|
||||||
|
timeZone: bookingTimeZone,
|
||||||
|
language: bookingLanguage,
|
||||||
|
metadata: bookingMetadata,
|
||||||
|
hashedLink: bookingHashedLink,
|
||||||
|
responses: bookingResponses,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
||||||
|
response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||||
|
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
|
||||||
|
expect(responseBody.data.id).toBeDefined();
|
||||||
|
expect(responseBody.data.uid).toBeDefined();
|
||||||
|
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||||
|
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||||
|
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||||
|
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||||
|
|
||||||
|
createdBooking = responseBody.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("should reschedule a booking", () => {
|
describe("should reschedule a booking", () => {
|
||||||
it("should reschedule with updated start time, end time & metadata", async () => {
|
it("should reschedule with updated start time, end time & metadata", async () => {
|
||||||
const newBookingStart = "2040-05-21T12:30:00.000Z";
|
const newBookingStart = "2040-05-21T12:30:00.000Z";
|
||||||
@@ -202,18 +306,22 @@ describe("Bookings Endpoints", () => {
|
|||||||
.get("/v2/bookings?filters[status]=upcoming")
|
.get("/v2/bookings?filters[status]=upcoming")
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseBody: GetBookingsOutput = response.body;
|
const responseBody: GetBookingsOutput = response.body;
|
||||||
const fetchedBooking = responseBody.data.bookings[0];
|
|
||||||
|
|
||||||
expect(responseBody.data.bookings.length).toEqual(1);
|
expect(responseBody.data.bookings.length).toEqual(2);
|
||||||
|
const fetchedBooking = responseBody.data.bookings.find(
|
||||||
|
(booking) => booking.id === createdBooking.id
|
||||||
|
);
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(fetchedBooking).toBeDefined();
|
expect(fetchedBooking).toBeDefined();
|
||||||
|
|
||||||
expect(fetchedBooking.id).toEqual(createdBooking.id);
|
if (fetchedBooking) {
|
||||||
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
expect(fetchedBooking.id).toEqual(createdBooking.id);
|
||||||
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
||||||
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
||||||
expect(fetchedBooking.user?.email).toEqual(userEmail);
|
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
||||||
|
expect(fetchedBooking.user?.email).toEqual(userEmail);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { MarkNoShowInput } from "@/ee/bookings/inputs/mark-no-show.input";
|
|||||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||||
import { MarkNoShowOutput } from "@/ee/bookings/outputs/mark-no-show.output";
|
import { MarkNoShowOutput } from "@/ee/bookings/outputs/mark-no-show.output";
|
||||||
|
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
@@ -28,10 +30,12 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { NextApiRequest } from "next/types";
|
import { NextApiRequest } from "next/types";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||||
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
|
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
|
||||||
@@ -46,7 +50,8 @@ import {
|
|||||||
getBookingInfo,
|
getBookingInfo,
|
||||||
handleCancelBooking,
|
handleCancelBooking,
|
||||||
getBookingForReschedule,
|
getBookingForReschedule,
|
||||||
} from "@calcom/platform-libraries-0.0.23";
|
ErrorCode,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
@@ -86,7 +91,9 @@ export class BookingsController {
|
|||||||
private readonly oAuthFlowService: OAuthFlowService,
|
private readonly oAuthFlowService: OAuthFlowService,
|
||||||
private readonly prismaReadService: PrismaReadService,
|
private readonly prismaReadService: PrismaReadService,
|
||||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||||
private readonly billingService: BillingService
|
private readonly billingService: BillingService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly apiKeyRepository: ApiKeyRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@@ -238,8 +245,15 @@ export class BookingsController {
|
|||||||
): Promise<ApiResponse<BookingResponse[]>> {
|
): Promise<ApiResponse<BookingResponse[]>> {
|
||||||
const oAuthClientId = clientId?.toString();
|
const oAuthClientId = clientId?.toString();
|
||||||
try {
|
try {
|
||||||
|
const recurringEventId = uuidv4();
|
||||||
|
for (const recurringEvent of req.body) {
|
||||||
|
if (!recurringEvent.recurringEventId) {
|
||||||
|
recurringEvent.recurringEventId = recurringEventId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
await this.createNextApiRecurringBookingRequest(req, oAuthClientId)
|
||||||
);
|
);
|
||||||
|
|
||||||
createdBookings.forEach(async (booking) => {
|
createdBookings.forEach(async (booking) => {
|
||||||
@@ -296,9 +310,17 @@ export class BookingsController {
|
|||||||
|
|
||||||
private async getOwnerId(req: Request): Promise<number | undefined> {
|
private async getOwnerId(req: Request): Promise<number | undefined> {
|
||||||
try {
|
try {
|
||||||
const accessToken = req.get("Authorization")?.replace("Bearer ", "");
|
const bearerToken = req.get("Authorization")?.replace("Bearer ", "");
|
||||||
if (accessToken) {
|
if (bearerToken) {
|
||||||
return this.oAuthFlowService.getOwnerId(accessToken);
|
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
|
||||||
|
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
|
||||||
|
const apiKeyHash = hashAPIKey(strippedApiKey);
|
||||||
|
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||||
|
return keyData?.userId;
|
||||||
|
} else {
|
||||||
|
// Access Token
|
||||||
|
return this.oAuthFlowService.getOwnerId(bearerToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
@@ -338,6 +360,19 @@ export class BookingsController {
|
|||||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async createNextApiRecurringBookingRequest(
|
||||||
|
req: BookingRequest,
|
||||||
|
oAuthClientId?: string,
|
||||||
|
platformBookingLocation?: string
|
||||||
|
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||||
|
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||||
|
const oAuthParams = oAuthClientId
|
||||||
|
? await this.getOAuthClientsParams(oAuthClientId)
|
||||||
|
: DEFAULT_PLATFORM_PARAMS;
|
||||||
|
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
|
||||||
|
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||||
|
}
|
||||||
|
|
||||||
private handleBookingErrors(
|
private handleBookingErrors(
|
||||||
err: Error | HttpError | unknown,
|
err: Error | HttpError | unknown,
|
||||||
type?: "recurring" | `instant` | "no-show"
|
type?: "recurring" | `instant` | "no-show"
|
||||||
@@ -353,6 +388,9 @@ export class BookingsController {
|
|||||||
|
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
|
if (Object.values(ErrorCode).includes(error.message as unknown as ErrorCode)) {
|
||||||
|
throw new HttpException(error.message, 400);
|
||||||
|
}
|
||||||
throw new InternalServerErrorException(error?.message ?? errMsg);
|
throw new InternalServerErrorException(error?.message ?? errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ export class CreateBookingInput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
rescheduleUid?: string;
|
rescheduleUid?: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
recurringEventId?: string;
|
|
||||||
|
|
||||||
@IsTimeZone()
|
@IsTimeZone()
|
||||||
timeZone!: string;
|
timeZone!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
|
||||||
|
|
||||||
import type { AppsStatus } from "@calcom/platform-libraries-0.0.23";
|
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@@ -21,4 +21,8 @@ export class CreateRecurringBookingInput extends CreateBookingInput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
currentRecurringIndex?: number;
|
currentRecurringIndex?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
recurringEventId?: string;
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ class Primary {
|
|||||||
credentialId!: number;
|
credentialId!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Calendar {
|
export class Calendar {
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
externalId!: string;
|
externalId!: string;
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ class Calendar {
|
|||||||
credentialId!: number;
|
credentialId!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConnectedCalendar {
|
export class ConnectedCalendar {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
integration!: Integration;
|
integration!: Integration;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||||
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.23";
|
import { symmetricEncrypt, CalendarService, symmetricDecrypt } from "@calcom/platform-libraries";
|
||||||
|
import { Credential } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppleCalendarService implements CredentialSyncCalendarApp {
|
export class AppleCalendarService implements CredentialSyncCalendarApp {
|
||||||
@@ -61,26 +62,70 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
|
|||||||
if (username.length <= 1 || password.length <= 1)
|
if (username.length <= 1 || password.length <= 1)
|
||||||
throw new BadRequestException(`Username or password cannot be empty`);
|
throw new BadRequestException(`Username or password cannot be empty`);
|
||||||
|
|
||||||
const data = {
|
const existingAppleCalendarCredentials = await this.credentialRepository.getAllUserCredentialsByTypeAndId(
|
||||||
type: APPLE_CALENDAR_TYPE,
|
APPLE_CALENDAR_TYPE,
|
||||||
key: symmetricEncrypt(
|
userId
|
||||||
JSON.stringify({ username, password }),
|
);
|
||||||
process.env.CALENDSO_ENCRYPTION_KEY || ""
|
|
||||||
),
|
let hasMatchingUsernameAndPassword = false;
|
||||||
userId: userId,
|
|
||||||
teamId: null,
|
if (existingAppleCalendarCredentials.length > 0) {
|
||||||
appId: APPLE_CALENDAR_ID,
|
const hasCalendarWithGivenCredentials = existingAppleCalendarCredentials.find(
|
||||||
invalid: false,
|
(calendarCredential: Credential) => {
|
||||||
};
|
const decryptedKey = JSON.parse(
|
||||||
|
symmetricDecrypt(calendarCredential.key as string, process.env.CALENDSO_ENCRYPTION_KEY || "")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (decryptedKey.username === username) {
|
||||||
|
if (decryptedKey.password === password) {
|
||||||
|
hasMatchingUsernameAndPassword = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!!hasCalendarWithGivenCredentials && hasMatchingUsernameAndPassword) {
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!hasCalendarWithGivenCredentials && !hasMatchingUsernameAndPassword) {
|
||||||
|
await this.credentialRepository.upsertAppCredential(
|
||||||
|
APPLE_CALENDAR_TYPE,
|
||||||
|
symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY || ""),
|
||||||
|
userId,
|
||||||
|
hasCalendarWithGivenCredentials.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const data = {
|
||||||
|
type: APPLE_CALENDAR_TYPE,
|
||||||
|
key: symmetricEncrypt(
|
||||||
|
JSON.stringify({ username, password }),
|
||||||
|
process.env.CALENDSO_ENCRYPTION_KEY || ""
|
||||||
|
),
|
||||||
|
userId: userId,
|
||||||
|
teamId: null,
|
||||||
|
appId: APPLE_CALENDAR_ID,
|
||||||
|
invalid: false,
|
||||||
|
};
|
||||||
|
|
||||||
const dav = new CalendarService({
|
const dav = new CalendarService({
|
||||||
id: 0,
|
id: 0,
|
||||||
...data,
|
...data,
|
||||||
user: { email: userEmail },
|
user: { email: userEmail },
|
||||||
});
|
});
|
||||||
await dav?.listCalendars();
|
await dav?.listCalendars();
|
||||||
await this.credentialRepository.createAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
|
await this.credentialRepository.upsertAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
throw new BadRequestException(`Could not add this apple calendar account: ${reason}`);
|
throw new BadRequestException(`Could not add this apple calendar account: ${reason}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@/modules/credentials/credentials.repository";
|
} from "@/modules/credentials/credentials.repository";
|
||||||
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 { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
@@ -15,10 +16,12 @@ import {
|
|||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.23";
|
import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
||||||
|
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
|
||||||
import { Calendar } from "@calcom/platform-types";
|
import { Calendar } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -33,7 +36,8 @@ export class CalendarsService {
|
|||||||
private readonly calendarsRepository: CalendarsRepository,
|
private readonly calendarsRepository: CalendarsRepository,
|
||||||
private readonly dbRead: PrismaReadService,
|
private readonly dbRead: PrismaReadService,
|
||||||
private readonly dbWrite: PrismaWriteService,
|
private readonly dbWrite: PrismaWriteService,
|
||||||
private readonly config: ConfigService
|
private readonly config: ConfigService,
|
||||||
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCalendars(userId: number) {
|
async getCalendars(userId: number) {
|
||||||
@@ -145,4 +149,32 @@ export class CalendarsService {
|
|||||||
throw new NotFoundException("Calendar credentials not found");
|
throw new NotFoundException("Calendar credentials not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createAndLinkCalendarEntry(
|
||||||
|
userId: number,
|
||||||
|
externalId: string,
|
||||||
|
key: Prisma.InputJsonValue,
|
||||||
|
calendarType: keyof typeof APPS_TYPE_ID_MAPPING,
|
||||||
|
credentialId?: number | null
|
||||||
|
) {
|
||||||
|
const credential = await this.credentialsRepository.upsertAppCredential(
|
||||||
|
calendarType,
|
||||||
|
key,
|
||||||
|
userId,
|
||||||
|
credentialId
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.selectedCalendarsRepository.upsertSelectedCalendar(
|
||||||
|
externalId,
|
||||||
|
credential.id,
|
||||||
|
userId,
|
||||||
|
calendarType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) {
|
||||||
|
const credential = await this.credentialsRepository.getUserCredentialById(userId, credentialId, type);
|
||||||
|
|
||||||
|
return !credential?.invalid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -131,11 +131,6 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
|||||||
const token = await oAuth2Client.getToken(parsedCode);
|
const token = await oAuth2Client.getToken(parsedCode);
|
||||||
// Google oAuth Credentials are stored in token.tokens
|
// Google oAuth Credentials are stored in token.tokens
|
||||||
const key = token.tokens;
|
const key = token.tokens;
|
||||||
const credential = await this.credentialRepository.createAppCredential(
|
|
||||||
GOOGLE_CALENDAR_TYPE,
|
|
||||||
key as Prisma.InputJsonValue,
|
|
||||||
ownerId
|
|
||||||
);
|
|
||||||
|
|
||||||
oAuth2Client.setCredentials(key);
|
oAuth2Client.setCredentials(key);
|
||||||
|
|
||||||
@@ -149,10 +144,39 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
|||||||
const primaryCal = cals.data.items?.find((cal) => cal.primary);
|
const primaryCal = cals.data.items?.find((cal) => cal.primary);
|
||||||
|
|
||||||
if (primaryCal?.id) {
|
if (primaryCal?.id) {
|
||||||
await this.selectedCalendarsRepository.createSelectedCalendar(
|
const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar(
|
||||||
primaryCal.id,
|
|
||||||
credential.id,
|
|
||||||
ownerId,
|
ownerId,
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
primaryCal.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyExistingSelectedCalendar) {
|
||||||
|
const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity(
|
||||||
|
ownerId,
|
||||||
|
alreadyExistingSelectedCalendar.credentialId ?? 0,
|
||||||
|
GOOGLE_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
// user credential probably got expired in this case
|
||||||
|
if (!isCredentialValid) {
|
||||||
|
await this.calendarsService.createAndLinkCalendarEntry(
|
||||||
|
ownerId,
|
||||||
|
alreadyExistingSelectedCalendar.externalId,
|
||||||
|
key as Prisma.InputJsonValue,
|
||||||
|
GOOGLE_CALENDAR_TYPE,
|
||||||
|
alreadyExistingSelectedCalendar.credentialId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: redir || origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.calendarsService.createAndLinkCalendarEntry(
|
||||||
|
ownerId,
|
||||||
|
primaryCal.id,
|
||||||
|
key as Prisma.InputJsonValue,
|
||||||
GOOGLE_CALENDAR_TYPE
|
GOOGLE_CALENDAR_TYPE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,16 +168,39 @@ export class OutlookService implements OAuthCalendarApp {
|
|||||||
const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token);
|
const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token);
|
||||||
|
|
||||||
if (defaultCalendar?.id) {
|
if (defaultCalendar?.id) {
|
||||||
const credential = await this.credentialRepository.createAppCredential(
|
const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar(
|
||||||
|
ownerId,
|
||||||
OFFICE_365_CALENDAR_TYPE,
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
office365OAuthCredentials,
|
defaultCalendar.id
|
||||||
ownerId
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.selectedCalendarsRepository.createSelectedCalendar(
|
if (alreadyExistingSelectedCalendar) {
|
||||||
defaultCalendar.id,
|
const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity(
|
||||||
credential.id,
|
ownerId,
|
||||||
|
alreadyExistingSelectedCalendar.credentialId ?? 0,
|
||||||
|
OFFICE_365_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
// user credential probably got expired in this case
|
||||||
|
if (!isCredentialValid) {
|
||||||
|
await this.calendarsService.createAndLinkCalendarEntry(
|
||||||
|
ownerId,
|
||||||
|
alreadyExistingSelectedCalendar.externalId,
|
||||||
|
office365OAuthCredentials,
|
||||||
|
OFFICE_365_CALENDAR_TYPE,
|
||||||
|
alreadyExistingSelectedCalendar.credentialId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: redir || origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.calendarsService.createAndLinkCalendarEntry(
|
||||||
ownerId,
|
ownerId,
|
||||||
|
defaultCalendar.id,
|
||||||
|
office365OAuthCredentials,
|
||||||
OFFICE_365_CALENDAR_TYPE
|
OFFICE_365_CALENDAR_TYPE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ import {
|
|||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
eventTypeBookingFields,
|
eventTypeBookingFields,
|
||||||
eventTypeLocations,
|
eventTypeLocations,
|
||||||
} from "@calcom/platform-libraries-0.0.23";
|
} from "@calcom/platform-libraries";
|
||||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Event types Endpoints", () => {
|
describe("Event types Endpoints", () => {
|
||||||
|
|||||||
+2
-2
@@ -36,7 +36,7 @@ import {
|
|||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
||||||
@@ -47,7 +47,7 @@ import { PrismaClient } from "@calcom/prisma";
|
|||||||
version: [VERSION_2024_04_15, VERSION_2024_06_11],
|
version: [VERSION_2024_04_15, VERSION_2024_06_11],
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Event types")
|
@DocsExcludeController(true)
|
||||||
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,
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { getEventTypeById } from "@calcom/platform-libraries-0.0.23";
|
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesRepository_2024_04_15 {
|
export class EventTypesRepository_2024_04_15 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(
|
||||||
|
private readonly dbRead: PrismaReadService,
|
||||||
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private usersService: UsersService
|
||||||
|
) {}
|
||||||
|
|
||||||
async createUserEventType(
|
async createUserEventType(
|
||||||
userId: number,
|
userId: number,
|
||||||
@@ -46,7 +51,7 @@ export class EventTypesRepository_2024_04_15 {
|
|||||||
eventTypeId: number
|
eventTypeId: number
|
||||||
) {
|
) {
|
||||||
return await getEventTypeById({
|
return await getEventTypeById({
|
||||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
currentOrganizationId: this.usersService.getUserMainOrgId(user),
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||||
|
|||||||
+4
@@ -177,6 +177,10 @@ export class BookingField_2024_04_15 {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => Source)
|
@Type(() => Source)
|
||||||
sources?: Source[];
|
sources?: Source[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
disableOnPrefill?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecurringEvent_2024_04_15 {
|
export class RecurringEvent_2024_04_15 {
|
||||||
|
|||||||
+4
@@ -87,6 +87,10 @@ class BookingField {
|
|||||||
@Type(() => Source)
|
@Type(() => Source)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
sources?: Source[];
|
sources?: Source[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
disableOnPrefill?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Organization {
|
class Organization {
|
||||||
|
|||||||
+19
-7
@@ -6,6 +6,7 @@ import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs
|
|||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.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";
|
||||||
|
|
||||||
@@ -14,7 +15,8 @@ import {
|
|||||||
updateEventType,
|
updateEventType,
|
||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
getEventTypesPublic,
|
getEventTypesPublic,
|
||||||
} from "@calcom/platform-libraries-0.0.23";
|
systemBeforeFieldEmail,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
import { EventType } from "@calcom/prisma/client";
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -24,7 +26,8 @@ export class EventTypesService_2024_04_15 {
|
|||||||
private readonly membershipsRepository: MembershipsRepository,
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
private readonly dbWrite: PrismaWriteService
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUserEventType(
|
async createUserEventType(
|
||||||
@@ -53,11 +56,11 @@ export class EventTypesService_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToCreateEvent(user: UserWithProfile) {
|
async getUserToCreateEvent(user: UserWithProfile) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
const isOrgAdmin = organizationId
|
const isOrgAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
: false;
|
: false;
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
@@ -80,7 +83,7 @@ export class EventTypesService_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
|
|
||||||
const isUserOrganizationAdmin = organizationId
|
const isUserOrganizationAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
@@ -125,8 +128,17 @@ export class EventTypesService_2024_04_15 {
|
|||||||
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
|
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
|
||||||
this.checkCanUpdateEventType(user.id, eventTypeId);
|
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||||
|
const bookingFields = [...(body.bookingFields || [])];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!bookingFields.find((field) => field.type === "email") &&
|
||||||
|
!bookingFields.find((field) => field.type === "phone")
|
||||||
|
) {
|
||||||
|
bookingFields.push(systemBeforeFieldEmail);
|
||||||
|
}
|
||||||
|
|
||||||
await updateEventType({
|
await updateEventType({
|
||||||
input: { id: eventTypeId, ...body },
|
input: { id: eventTypeId, ...body, bookingFields },
|
||||||
ctx: {
|
ctx: {
|
||||||
user: eventTypeUser,
|
user: eventTypeUser,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
@@ -153,7 +165,7 @@ export class EventTypesService_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||||
}
|
}
|
||||||
|
|||||||
+425
-1
@@ -1,5 +1,6 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
@@ -19,12 +20,14 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
|
||||||
|
import { BookingWindowPeriodInputTypeEnum_2024_06_14, FrequencyInput } from "@calcom/platform-enums";
|
||||||
import {
|
import {
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
describe("Event types Endpoints", () => {
|
describe("Event types Endpoints", () => {
|
||||||
describe("Not authenticated", () => {
|
describe("Not authenticated", () => {
|
||||||
@@ -212,6 +215,26 @@ describe("Event types Endpoints", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
scheduleId: firstSchedule.id,
|
scheduleId: firstSchedule.id,
|
||||||
|
bookingLimitsCount: {
|
||||||
|
day: 2,
|
||||||
|
week: 5,
|
||||||
|
},
|
||||||
|
onlyShowFirstAvailableSlot: true,
|
||||||
|
bookingLimitsDuration: {
|
||||||
|
day: 60,
|
||||||
|
week: 100,
|
||||||
|
},
|
||||||
|
offsetStart: 30,
|
||||||
|
bookingWindow: {
|
||||||
|
type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays,
|
||||||
|
value: 30,
|
||||||
|
rolling: true,
|
||||||
|
},
|
||||||
|
recurrence: {
|
||||||
|
frequency: FrequencyInput.weekly,
|
||||||
|
interval: 2,
|
||||||
|
occurrences: 10,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -227,10 +250,24 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(createdEventType.description).toEqual(body.description);
|
expect(createdEventType.description).toEqual(body.description);
|
||||||
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
||||||
expect(createdEventType.locations).toEqual(body.locations);
|
expect(createdEventType.locations).toEqual(body.locations);
|
||||||
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
|
|
||||||
expect(createdEventType.ownerId).toEqual(user.id);
|
expect(createdEventType.ownerId).toEqual(user.id);
|
||||||
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
|
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
|
||||||
|
expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||||
|
expect(createdEventType.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||||
|
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||||
|
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
|
||||||
|
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
|
||||||
|
expect(createdEventType.recurrence).toEqual(body.recurrence);
|
||||||
|
|
||||||
|
const responseBookingFields = body.bookingFields || [];
|
||||||
|
const expectedBookingFields = [
|
||||||
|
{ isDefault: true, required: true, slug: "name", type: "name" },
|
||||||
|
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||||
|
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||||
|
...responseBookingFields.map((field) => ({ isDefault: false, ...field })),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||||
eventType = responseBody.data;
|
eventType = responseBody.data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -241,6 +278,26 @@ describe("Event types Endpoints", () => {
|
|||||||
const body: UpdateEventTypeInput_2024_06_14 = {
|
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||||
title: newTitle,
|
title: newTitle,
|
||||||
scheduleId: secondSchedule.id,
|
scheduleId: secondSchedule.id,
|
||||||
|
bookingLimitsCount: {
|
||||||
|
day: 4,
|
||||||
|
week: 10,
|
||||||
|
},
|
||||||
|
onlyShowFirstAvailableSlot: true,
|
||||||
|
bookingLimitsDuration: {
|
||||||
|
day: 100,
|
||||||
|
week: 200,
|
||||||
|
},
|
||||||
|
offsetStart: 50,
|
||||||
|
bookingWindow: {
|
||||||
|
type: BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays,
|
||||||
|
value: 40,
|
||||||
|
rolling: false,
|
||||||
|
},
|
||||||
|
recurrence: {
|
||||||
|
frequency: FrequencyInput.monthly,
|
||||||
|
interval: 4,
|
||||||
|
occurrences: 10,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -261,9 +318,21 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
|
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||||
expect(updatedEventType.ownerId).toEqual(user.id);
|
expect(updatedEventType.ownerId).toEqual(user.id);
|
||||||
expect(updatedEventType.scheduleId).toEqual(secondSchedule.id);
|
expect(updatedEventType.scheduleId).toEqual(secondSchedule.id);
|
||||||
|
expect(updatedEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||||
|
expect(updatedEventType.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||||
|
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||||
|
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
|
||||||
|
expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow);
|
||||||
|
expect(updatedEventType.recurrence).toEqual(body.recurrence);
|
||||||
|
|
||||||
eventType.title = newTitle;
|
eventType.title = newTitle;
|
||||||
eventType.scheduleId = secondSchedule.id;
|
eventType.scheduleId = secondSchedule.id;
|
||||||
|
eventType.bookingLimitsCount = updatedEventType.bookingLimitsCount;
|
||||||
|
eventType.onlyShowFirstAvailableSlot = updatedEventType.onlyShowFirstAvailableSlot;
|
||||||
|
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
|
||||||
|
eventType.offsetStart = updatedEventType.offsetStart;
|
||||||
|
eventType.bookingWindow = updatedEventType.bookingWindow;
|
||||||
|
eventType.recurrence = updatedEventType.recurrence;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -299,6 +368,12 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(fetchedEventType.locations).toEqual(eventType.locations);
|
expect(fetchedEventType.locations).toEqual(eventType.locations);
|
||||||
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
|
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||||
expect(fetchedEventType.ownerId).toEqual(user.id);
|
expect(fetchedEventType.ownerId).toEqual(user.id);
|
||||||
|
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||||
|
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||||
|
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
|
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||||
|
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||||
|
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/even-types by username`, async () => {
|
it(`/GET/even-types by username`, async () => {
|
||||||
@@ -324,6 +399,12 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
|
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||||
|
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||||
|
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
|
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||||
|
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||||
|
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/event-types by username and eventSlug`, async () => {
|
it(`/GET/event-types by username and eventSlug`, async () => {
|
||||||
@@ -344,6 +425,12 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||||
|
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||||
|
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||||
|
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
|
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||||
|
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||||
|
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/:id not existing`, async () => {
|
it(`/GET/:id not existing`, async () => {
|
||||||
@@ -380,4 +467,341 @@ describe("Event types Endpoints", () => {
|
|||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Handle legacy event-types booking fields", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let organization: Team;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "legacy-event-types-test-e2e@api.com";
|
||||||
|
const name = "bob-the-builder";
|
||||||
|
const username = name;
|
||||||
|
let user: User;
|
||||||
|
let legacyEventTypeId1: number;
|
||||||
|
let legacyEventTypeId2: number;
|
||||||
|
|
||||||
|
const expectedReturnSystemFields = [
|
||||||
|
{ isDefault: true, required: true, slug: "name", type: "name" },
|
||||||
|
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||||
|
{ isDefault: true, type: "radioInput", slug: "location", required: false },
|
||||||
|
{ isDefault: true, required: true, slug: "title", type: "text" },
|
||||||
|
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
|
||||||
|
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
|
||||||
|
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
name,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: ["redirect-uri"],
|
||||||
|
permissions: 32,
|
||||||
|
};
|
||||||
|
const secret = "secret";
|
||||||
|
|
||||||
|
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(oAuthClient).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow creating an event type with input of event-types version 2024_04_15", async () => {
|
||||||
|
const body: CreateEventTypeInput_2024_04_15 = {
|
||||||
|
title: "Coding class",
|
||||||
|
slug: "coding-class",
|
||||||
|
description: "Let's learn how to code like a pro.",
|
||||||
|
length: 60,
|
||||||
|
locations: [{ type: "integrations:daily" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return system bookingFields stored in database", async () => {
|
||||||
|
const legacyEventTypeInput = {
|
||||||
|
title: "legacy event type",
|
||||||
|
description: "legacy event type description",
|
||||||
|
length: 40,
|
||||||
|
hidden: false,
|
||||||
|
slug: "legacy-event-type",
|
||||||
|
locations: [],
|
||||||
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
|
bookingFields: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
type: "name",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system",
|
||||||
|
required: true,
|
||||||
|
defaultLabel: "your_name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email",
|
||||||
|
type: "email",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system",
|
||||||
|
required: true,
|
||||||
|
defaultLabel: "email_address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "location",
|
||||||
|
type: "radioInput",
|
||||||
|
label: "",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system",
|
||||||
|
required: false,
|
||||||
|
placeholder: "",
|
||||||
|
defaultLabel: "location",
|
||||||
|
getOptionsAt: "locations",
|
||||||
|
optionsInputs: {
|
||||||
|
phone: { type: "phone", required: true, placeholder: "" },
|
||||||
|
attendeeInPerson: { type: "address", required: true, placeholder: "" },
|
||||||
|
},
|
||||||
|
hideWhenJustOneOption: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
hidden: true,
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: true,
|
||||||
|
defaultLabel: "what_is_this_meeting_about",
|
||||||
|
defaultPlaceholder: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "notes",
|
||||||
|
type: "textarea",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: false,
|
||||||
|
defaultLabel: "additional_notes",
|
||||||
|
defaultPlaceholder: "share_additional_notes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "guests",
|
||||||
|
type: "multiemail",
|
||||||
|
hidden: false,
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: false,
|
||||||
|
defaultLabel: "additional_guests",
|
||||||
|
defaultPlaceholder: "email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rescheduleReason",
|
||||||
|
type: "textarea",
|
||||||
|
views: [{ id: "reschedule", label: "Reschedule View" }],
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: false,
|
||||||
|
defaultLabel: "reason_for_reschedule",
|
||||||
|
defaultPlaceholder: "reschedule_placeholder",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||||
|
legacyEventTypeId1 = legacyEventType.id;
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types/${legacyEventTypeId1}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const fetchedEventType = responseBody.data;
|
||||||
|
expect(fetchedEventType.bookingFields).toEqual(expectedReturnSystemFields);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return user created bookingFields with system fields", async () => {
|
||||||
|
const userDefinedBookingField = {
|
||||||
|
name: "team",
|
||||||
|
type: "textarea",
|
||||||
|
label: "your team",
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
id: "user",
|
||||||
|
type: "user",
|
||||||
|
label: "User",
|
||||||
|
fieldRequired: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
editable: "user",
|
||||||
|
required: true,
|
||||||
|
placeholder: "FC Barcelona",
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyEventTypeInput = {
|
||||||
|
title: "legacy event type two",
|
||||||
|
description: "legacy event type description two",
|
||||||
|
length: 40,
|
||||||
|
hidden: false,
|
||||||
|
slug: "legacy-event-type-two",
|
||||||
|
locations: [],
|
||||||
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
|
bookingFields: [
|
||||||
|
userDefinedBookingField,
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
type: "name",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system",
|
||||||
|
required: true,
|
||||||
|
defaultLabel: "your_name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email",
|
||||||
|
type: "email",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system",
|
||||||
|
required: true,
|
||||||
|
defaultLabel: "email_address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "location",
|
||||||
|
type: "radioInput",
|
||||||
|
label: "",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system",
|
||||||
|
required: false,
|
||||||
|
placeholder: "",
|
||||||
|
defaultLabel: "location",
|
||||||
|
getOptionsAt: "locations",
|
||||||
|
optionsInputs: {
|
||||||
|
phone: { type: "phone", required: true, placeholder: "" },
|
||||||
|
attendeeInPerson: { type: "address", required: true, placeholder: "" },
|
||||||
|
},
|
||||||
|
hideWhenJustOneOption: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
hidden: true,
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: true,
|
||||||
|
defaultLabel: "what_is_this_meeting_about",
|
||||||
|
defaultPlaceholder: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "notes",
|
||||||
|
type: "textarea",
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: false,
|
||||||
|
defaultLabel: "additional_notes",
|
||||||
|
defaultPlaceholder: "share_additional_notes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "guests",
|
||||||
|
type: "multiemail",
|
||||||
|
hidden: false,
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: false,
|
||||||
|
defaultLabel: "additional_guests",
|
||||||
|
defaultPlaceholder: "email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rescheduleReason",
|
||||||
|
type: "textarea",
|
||||||
|
views: [{ id: "reschedule", label: "Reschedule View" }],
|
||||||
|
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||||
|
editable: "system-but-optional",
|
||||||
|
required: false,
|
||||||
|
defaultLabel: "reason_for_reschedule",
|
||||||
|
defaultPlaceholder: "reschedule_placeholder",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||||
|
legacyEventTypeId2 = legacyEventType.id;
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types/${legacyEventTypeId2}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const fetchedEventType = responseBody.data;
|
||||||
|
|
||||||
|
expect(fetchedEventType.bookingFields).toEqual([
|
||||||
|
...expectedReturnSystemFields,
|
||||||
|
{
|
||||||
|
isDefault: false,
|
||||||
|
type: userDefinedBookingField.type,
|
||||||
|
slug: userDefinedBookingField.name,
|
||||||
|
label: userDefinedBookingField.label,
|
||||||
|
required: userDefinedBookingField.required,
|
||||||
|
placeholder: userDefinedBookingField.placeholder,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
try {
|
||||||
|
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||||
|
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
||||||
|
} catch (e) {
|
||||||
|
// Event type might have been deleted by the test
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await userRepositoryFixture.delete(user.id);
|
||||||
|
} catch (e) {
|
||||||
|
// User might have been deleted by the test
|
||||||
|
}
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+30
-1
@@ -24,7 +24,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import {
|
import {
|
||||||
@@ -39,12 +39,23 @@ import {
|
|||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Event types")
|
@DocsTags("Event types")
|
||||||
|
@ApiHeader({
|
||||||
|
name: "cal-api-version",
|
||||||
|
description: `Must be set to \`2024-06-14\``,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
export class EventTypesController_2024_06_14 {
|
export class EventTypesController_2024_06_14 {
|
||||||
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
|
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiHeader({
|
||||||
|
name: "Authorization",
|
||||||
|
description:
|
||||||
|
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
async createEventType(
|
async createEventType(
|
||||||
@Body() body: CreateEventTypeInput_2024_06_14,
|
@Body() body: CreateEventTypeInput_2024_06_14,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
@@ -60,6 +71,12 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Get("/:eventTypeId")
|
@Get("/:eventTypeId")
|
||||||
@Permissions([EVENT_TYPE_READ])
|
@Permissions([EVENT_TYPE_READ])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiHeader({
|
||||||
|
name: "Authorization",
|
||||||
|
description:
|
||||||
|
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
async getEventTypeById(
|
async getEventTypeById(
|
||||||
@Param("eventTypeId") eventTypeId: string,
|
@Param("eventTypeId") eventTypeId: string,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
@@ -91,6 +108,12 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Patch("/:eventTypeId")
|
@Patch("/:eventTypeId")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiHeader({
|
||||||
|
name: "Authorization",
|
||||||
|
description:
|
||||||
|
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async updateEventType(
|
async updateEventType(
|
||||||
@Param("eventTypeId") eventTypeId: number,
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
@@ -108,6 +131,12 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Delete("/:eventTypeId")
|
@Delete("/:eventTypeId")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiHeader({
|
||||||
|
name: "Authorization",
|
||||||
|
description:
|
||||||
|
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
async deleteEventType(
|
async deleteEventType(
|
||||||
@Param("eventTypeId") eventTypeId: number,
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
@GetUser("id") userId: number
|
@GetUser("id") userId: number
|
||||||
|
|||||||
@@ -1,29 +1,46 @@
|
|||||||
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 { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getEventTypeById,
|
getEventTypeById,
|
||||||
transformApiEventTypeBookingFields,
|
transformBookingFieldsApiToInternal,
|
||||||
transformApiEventTypeLocations,
|
transformLocationsApiToInternal,
|
||||||
} from "@calcom/platform-libraries-0.0.23";
|
} from "@calcom/platform-libraries";
|
||||||
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
type InputEventTransformed = Omit<
|
type InputEventTransformed = Omit<
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
"lengthInMinutes" | "locations" | "bookingFields"
|
| "lengthInMinutes"
|
||||||
|
| "locations"
|
||||||
|
| "bookingFields"
|
||||||
|
| "bookingLimitsCount"
|
||||||
|
| "onlyShowFirstAvailableSlot"
|
||||||
|
| "bookingLimitsDuration"
|
||||||
|
| "offsetStart"
|
||||||
|
| "periodType"
|
||||||
|
| "periodDays"
|
||||||
|
| "periodCountCalendarDays"
|
||||||
|
| "periodStartDate"
|
||||||
|
| "periodEndDate"
|
||||||
|
| "recurrence"
|
||||||
> & {
|
> & {
|
||||||
length: number;
|
length: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
locations?: ReturnType<typeof transformApiEventTypeLocations>;
|
locations?: ReturnType<typeof transformLocationsApiToInternal>;
|
||||||
bookingFields?: ReturnType<typeof transformApiEventTypeBookingFields>;
|
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesRepository_2024_06_14 {
|
export class EventTypesRepository_2024_06_14 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(
|
||||||
|
private readonly dbRead: PrismaReadService,
|
||||||
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private usersService: UsersService
|
||||||
|
) {}
|
||||||
|
|
||||||
async createUserEventType(userId: number, body: InputEventTransformed) {
|
async createUserEventType(userId: number, body: InputEventTransformed) {
|
||||||
return this.dbWrite.prisma.eventType.create({
|
return this.dbWrite.prisma.eventType.create({
|
||||||
@@ -69,7 +86,7 @@ export class EventTypesRepository_2024_06_14 {
|
|||||||
eventTypeId: number
|
eventTypeId: number
|
||||||
) {
|
) {
|
||||||
return await getEventTypeById({
|
return await getEventTypeById({
|
||||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
currentOrganizationId: this.usersService.getUserMainOrgId(user),
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||||
|
import { Transform } from "class-transformer";
|
||||||
|
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";
|
||||||
|
|
||||||
|
export enum TemplateType {
|
||||||
|
CHECK_IN_APPOINTMENT = "CHECK_IN_APPOINTMENT",
|
||||||
|
CUSTOM_TEMPLATE = "CUSTOM_TEMPLATE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreatePhoneCallInput {
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\+[1-9]\d{1,14}$/, {
|
||||||
|
message:
|
||||||
|
"Invalid phone number format. Expected format: +<CountryCode><PhoneNumber> with no spaces or separators.",
|
||||||
|
})
|
||||||
|
@DocsProperty({ description: "Your phone number" })
|
||||||
|
yourPhoneNumber!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\+[1-9]\d{1,14}$/, {
|
||||||
|
message:
|
||||||
|
"Invalid phone number format. Expected format: +<CountryCode><PhoneNumber> with no spaces or separators.",
|
||||||
|
})
|
||||||
|
@DocsProperty({ description: "Number to call" })
|
||||||
|
numberToCall!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@DocsProperty({ description: "CAL API Key" })
|
||||||
|
calApiKey!: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@DocsProperty({ description: "Enabled status", default: true })
|
||||||
|
enabled = true;
|
||||||
|
|
||||||
|
@IsEnum(TemplateType)
|
||||||
|
@DocsProperty({ description: "Template type", enum: TemplateType })
|
||||||
|
templateType: TemplateType = TemplateType.CUSTOM_TEMPLATE;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@DocsProperty({ description: "Scheduler name" })
|
||||||
|
schedulerName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Transform(({ value }) => (value ? value : undefined))
|
||||||
|
@DocsProperty({ description: "Guest name" })
|
||||||
|
guestName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Transform(({ value }) => (value ? value : undefined))
|
||||||
|
@DocsProperty({ description: "Guest email" })
|
||||||
|
guestEmail?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Transform(({ value }) => (value ? value : undefined))
|
||||||
|
@DocsProperty({ description: "Guest company" })
|
||||||
|
guestCompany?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@DocsProperty({ description: "Begin message" })
|
||||||
|
beginMessage?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@DocsProperty({ description: "General prompt" })
|
||||||
|
generalPrompt?: string;
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
class Data {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
callId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
agentId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreatePhoneCallOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: Data,
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Data)
|
||||||
|
data!: Data;
|
||||||
|
}
|
||||||
+42
-8
@@ -10,9 +10,9 @@ 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 { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.23";
|
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.23";
|
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||||
import { dynamicEvent } from "@calcom/platform-libraries-0.0.23";
|
import { dynamicEvent } from "@calcom/platform-libraries";
|
||||||
import {
|
import {
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
@@ -38,7 +38,17 @@ export class EventTypesService_2024_06_14 {
|
|||||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
|
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
|
||||||
await this.checkCanCreateEventType(user.id, body);
|
await this.checkCanCreateEventType(user.id, body);
|
||||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||||
const bodyTransformed = this.inputEventTypesService.transformInputCreateEventType(body);
|
const {
|
||||||
|
bookingLimits,
|
||||||
|
durationLimits,
|
||||||
|
periodType = undefined,
|
||||||
|
periodDays = undefined,
|
||||||
|
periodCountCalendarDays = undefined,
|
||||||
|
periodStartDate = undefined,
|
||||||
|
periodEndDate = undefined,
|
||||||
|
recurrence = undefined,
|
||||||
|
...bodyTransformed
|
||||||
|
} = this.inputEventTypesService.transformInputCreateEventType(body);
|
||||||
const { eventType: eventTypeCreated } = await createEventType({
|
const { eventType: eventTypeCreated } = await createEventType({
|
||||||
input: bodyTransformed,
|
input: bodyTransformed,
|
||||||
ctx: {
|
ctx: {
|
||||||
@@ -49,6 +59,27 @@ export class EventTypesService_2024_06_14 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await updateEventType({
|
||||||
|
input: {
|
||||||
|
id: eventTypeCreated.id,
|
||||||
|
bookingLimits,
|
||||||
|
durationLimits,
|
||||||
|
periodType,
|
||||||
|
periodDays,
|
||||||
|
periodCountCalendarDays,
|
||||||
|
periodStartDate,
|
||||||
|
periodEndDate,
|
||||||
|
recurrence,
|
||||||
|
...bodyTransformed,
|
||||||
|
},
|
||||||
|
ctx: {
|
||||||
|
user: eventTypeUser,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
prisma: this.dbWrite.prisma,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id);
|
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id);
|
||||||
|
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
@@ -90,18 +121,21 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToCreateEvent(user: UserWithProfile) {
|
async getUserToCreateEvent(user: UserWithProfile) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
const isOrgAdmin = organizationId
|
const isOrgAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
: false;
|
: false;
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
username: user.username,
|
||||||
organizationId: user.organizationId,
|
organizationId: user.organizationId,
|
||||||
organization: { isOrgAdmin },
|
organization: { isOrgAdmin },
|
||||||
profile: { id: profileId },
|
profile: { id: profileId },
|
||||||
metadata: user.metadata,
|
metadata: user.metadata,
|
||||||
|
selectedCalendars,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +161,7 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
|
|
||||||
const isUserOrganizationAdmin = organizationId
|
const isUserOrganizationAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
@@ -238,7 +272,7 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-10
@@ -1,9 +1,16 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
transformApiEventTypeBookingFields,
|
transformBookingFieldsApiToInternal,
|
||||||
transformApiEventTypeLocations,
|
transformLocationsApiToInternal,
|
||||||
} from "@calcom/platform-libraries-0.0.23";
|
transformIntervalLimitsApiToInternal,
|
||||||
|
transformFutureBookingLimitsApiToInternal,
|
||||||
|
transformRecurrenceApiToInternal,
|
||||||
|
systemBeforeFieldName,
|
||||||
|
systemBeforeFieldEmail,
|
||||||
|
systemAfterFieldRescheduleReason,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
import { systemBeforeFieldLocation } from "@calcom/platform-libraries";
|
||||||
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -16,37 +23,98 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { lengthInMinutes, locations, bookingFields, ...rest } = inputEventType;
|
const {
|
||||||
|
lengthInMinutes,
|
||||||
|
locations,
|
||||||
|
bookingFields,
|
||||||
|
bookingLimitsCount,
|
||||||
|
bookingLimitsDuration,
|
||||||
|
bookingWindow,
|
||||||
|
recurrence,
|
||||||
|
...rest
|
||||||
|
} = inputEventType;
|
||||||
|
|
||||||
|
const hasMultipleLocations = (locations || defaultLocations).length > 1;
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...rest,
|
...rest,
|
||||||
length: lengthInMinutes,
|
length: lengthInMinutes,
|
||||||
locations: this.transformInputLocations(locations || defaultLocations),
|
locations: this.transformInputLocations(locations || defaultLocations),
|
||||||
bookingFields: this.transformInputBookingFields(bookingFields),
|
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
|
||||||
|
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||||
|
durationLimits: bookingLimitsDuration
|
||||||
|
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||||
|
: undefined,
|
||||||
|
...this.transformInputBookingWindow(bookingWindow),
|
||||||
|
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return eventType;
|
return eventType;
|
||||||
}
|
}
|
||||||
|
|
||||||
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
|
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
|
||||||
const { lengthInMinutes, locations, bookingFields, scheduleId, ...rest } = inputEventType;
|
const {
|
||||||
|
lengthInMinutes,
|
||||||
|
locations,
|
||||||
|
bookingFields,
|
||||||
|
scheduleId,
|
||||||
|
bookingLimitsCount,
|
||||||
|
bookingLimitsDuration,
|
||||||
|
bookingWindow,
|
||||||
|
recurrence,
|
||||||
|
...rest
|
||||||
|
} = inputEventType;
|
||||||
|
|
||||||
|
const hasMultipleLocations = !!(locations && locations?.length > 1);
|
||||||
|
|
||||||
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: bookingFields
|
||||||
|
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
|
||||||
|
: undefined,
|
||||||
schedule: scheduleId,
|
schedule: scheduleId,
|
||||||
|
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||||
|
durationLimits: bookingLimitsDuration
|
||||||
|
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||||
|
: undefined,
|
||||||
|
...this.transformInputBookingWindow(bookingWindow),
|
||||||
|
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return eventType;
|
return eventType;
|
||||||
}
|
}
|
||||||
|
|
||||||
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
|
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
|
||||||
return transformApiEventTypeLocations(inputLocations);
|
return transformLocationsApiToInternal(inputLocations);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
|
transformInputBookingFields(
|
||||||
return transformApiEventTypeBookingFields(inputBookingFields);
|
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
|
||||||
|
hasMultipleLocations: boolean
|
||||||
|
) {
|
||||||
|
const defaultFieldsBefore = [systemBeforeFieldName, systemBeforeFieldEmail];
|
||||||
|
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
|
||||||
|
if (hasMultipleLocations) {
|
||||||
|
defaultFieldsBefore.push(systemBeforeFieldLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customFields = transformBookingFieldsApiToInternal(inputBookingFields);
|
||||||
|
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
|
||||||
|
|
||||||
|
return [...defaultFieldsBefore, ...customFields, ...defaultFieldsAfter];
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
|
||||||
|
return transformIntervalLimitsApiToInternal(inputBookingFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
|
||||||
|
const res = transformFutureBookingLimitsApiToInternal(inputBookingWindow);
|
||||||
|
return !!res ? res : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
|
||||||
|
return transformRecurrenceApiToInternal(recurrence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+57
-11
@@ -4,12 +4,19 @@ import type { EventType, User, Schedule } from "@prisma/client";
|
|||||||
import {
|
import {
|
||||||
EventTypeMetaDataSchema,
|
EventTypeMetaDataSchema,
|
||||||
userMetadata,
|
userMetadata,
|
||||||
getResponseEventTypeLocations,
|
transformLocationsInternalToApi,
|
||||||
getResponseEventTypeBookingFields,
|
transformBookingFieldsInternalToApi,
|
||||||
parseRecurringEvent,
|
parseRecurringEvent,
|
||||||
TransformedLocationsSchema,
|
TransformedLocationsSchema,
|
||||||
BookingFieldsSchema,
|
BookingFieldsSchema,
|
||||||
} from "@calcom/platform-libraries-0.0.23";
|
SystemField,
|
||||||
|
CustomField,
|
||||||
|
parseBookingLimit,
|
||||||
|
transformIntervalLimitsInternalToApi,
|
||||||
|
transformFutureBookingLimitsInternalToApi,
|
||||||
|
transformRecurrenceInternalToApi,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
|
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
|
||||||
type DatabaseEventType = EventType & EventTypeRelations;
|
type DatabaseEventType = EventType & EventTypeRelations;
|
||||||
@@ -42,6 +49,15 @@ type Input = Pick<
|
|||||||
| "metadata"
|
| "metadata"
|
||||||
| "users"
|
| "users"
|
||||||
| "scheduleId"
|
| "scheduleId"
|
||||||
|
| "bookingLimits"
|
||||||
|
| "durationLimits"
|
||||||
|
| "onlyShowFirstAvailableSlot"
|
||||||
|
| "offsetStart"
|
||||||
|
| "periodType"
|
||||||
|
| "periodDays"
|
||||||
|
| "periodCountCalendarDays"
|
||||||
|
| "periodStartDate"
|
||||||
|
| "periodEndDate"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -69,13 +85,26 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
seatsShowAvailabilityCount,
|
seatsShowAvailabilityCount,
|
||||||
isInstantEvent,
|
isInstantEvent,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
|
onlyShowFirstAvailableSlot,
|
||||||
|
offsetStart,
|
||||||
} = databaseEventType;
|
} = databaseEventType;
|
||||||
|
|
||||||
const locations = this.transformLocations(databaseEventType.locations);
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
const bookingFields = this.transformBookingFields(databaseEventType.bookingFields);
|
const bookingFields = databaseEventType.bookingFields
|
||||||
const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
|
||||||
|
: [];
|
||||||
|
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||||
const users = this.transformUsers(databaseEventType.users);
|
const users = this.transformUsers(databaseEventType.users);
|
||||||
|
const bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits);
|
||||||
|
const bookingLimitsDuration = this.transformIntervalLimits(databaseEventType.durationLimits);
|
||||||
|
const bookingWindow = this.transformBookingWindow({
|
||||||
|
periodType: databaseEventType.periodType,
|
||||||
|
periodDays: databaseEventType.periodDays,
|
||||||
|
periodCountCalendarDays: databaseEventType.periodCountCalendarDays,
|
||||||
|
periodStartDate: databaseEventType.periodStartDate,
|
||||||
|
periodEndDate: databaseEventType.periodEndDate,
|
||||||
|
} as TransformFutureBookingsLimitSchema_2024_06_14);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -86,7 +115,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
description: description || "",
|
description: description || "",
|
||||||
locations,
|
locations,
|
||||||
bookingFields,
|
bookingFields,
|
||||||
recurringEvent,
|
recurrence,
|
||||||
disableGuests,
|
disableGuests,
|
||||||
slotInterval,
|
slotInterval,
|
||||||
minimumBookingNotice,
|
minimumBookingNotice,
|
||||||
@@ -105,22 +134,30 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
isInstantEvent,
|
isInstantEvent,
|
||||||
users,
|
users,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
|
bookingLimitsCount,
|
||||||
|
bookingLimitsDuration,
|
||||||
|
onlyShowFirstAvailableSlot,
|
||||||
|
offsetStart,
|
||||||
|
bookingWindow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
transformLocations(locations: any) {
|
transformLocations(locations: any) {
|
||||||
if (!locations) return [];
|
if (!locations) return [];
|
||||||
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
|
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
|
||||||
}
|
}
|
||||||
|
|
||||||
transformBookingFields(inputBookingFields: any) {
|
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
|
||||||
if (!inputBookingFields) return [];
|
if (!bookingFields) return [];
|
||||||
return getResponseEventTypeBookingFields(BookingFieldsSchema.parse(inputBookingFields));
|
|
||||||
|
return transformBookingFieldsInternalToApi(bookingFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformRecurringEvent(recurringEvent: any) {
|
transformRecurringEvent(recurringEvent: any) {
|
||||||
if (!recurringEvent) return null;
|
if (!recurringEvent) return null;
|
||||||
return parseRecurringEvent(recurringEvent);
|
const recurringEventParsed = parseRecurringEvent(recurringEvent);
|
||||||
|
if (!recurringEventParsed) return null;
|
||||||
|
return transformRecurrenceInternalToApi(recurringEventParsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformMetadata(metadata: any) {
|
transformMetadata(metadata: any) {
|
||||||
@@ -143,4 +180,13 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transformIntervalLimits(bookingLimits: any) {
|
||||||
|
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
|
||||||
|
return transformIntervalLimitsInternalToApi(bookingLimitsParsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
|
||||||
|
return transformFutureBookingLimitsInternalToApi(bookingLimits);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,10 @@ import { UsersModule } from "@/modules/users/users.module";
|
|||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { User } from "@prisma/client";
|
import { User, Team } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
@@ -25,9 +27,11 @@ describe("Me Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||||
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
const userEmail = "me-controller-e2e@api.com";
|
const userEmail = "me-controller-e2e@api.com";
|
||||||
let user: User;
|
let user: User;
|
||||||
|
let org: Team;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
@@ -43,6 +47,9 @@ describe("Me Endpoints", () => {
|
|||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
@@ -50,6 +57,20 @@ describe("Me Endpoints", () => {
|
|||||||
username: userEmail,
|
username: userEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: "Test org team",
|
||||||
|
isOrganization: true,
|
||||||
|
isPlatform: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await profilesRepositoryFixture.create({
|
||||||
|
uid: "asd-asd",
|
||||||
|
username: userEmail,
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
organization: { connect: { id: org.id } },
|
||||||
|
movedFromUser: { connect: { id: user.id } },
|
||||||
|
});
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
bootstrap(app as NestExpressApplication);
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
@@ -75,6 +96,8 @@ describe("Me Endpoints", () => {
|
|||||||
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
|
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
|
||||||
expect(responseBody.data.weekStart).toEqual(user.weekStart);
|
expect(responseBody.data.weekStart).toEqual(user.weekStart);
|
||||||
expect(responseBody.data.timeZone).toEqual(user.timeZone);
|
expect(responseBody.data.timeZone).toEqual(user.timeZone);
|
||||||
|
expect(responseBody.data.organization?.isPlatform).toEqual(true);
|
||||||
|
expect(responseBody.data.organization?.id).toEqual(org.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,6 +161,7 @@ describe("Me Endpoints", () => {
|
|||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
|||||||
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 { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
|
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 { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
|
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
@@ -23,14 +24,26 @@ import { userSchemaResponse } from "@calcom/platform-types";
|
|||||||
export class MeController {
|
export class MeController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly schedulesService: SchedulesService_2024_04_15
|
private readonly schedulesService: SchedulesService_2024_04_15,
|
||||||
|
private readonly usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([PROFILE_READ])
|
@Permissions([PROFILE_READ])
|
||||||
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
|
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
|
||||||
const me = userSchemaResponse.parse(user);
|
const organization = this.usersService.getUserMainProfile(user)?.organization;
|
||||||
|
const me = userSchemaResponse.parse(
|
||||||
|
organization
|
||||||
|
? {
|
||||||
|
...user,
|
||||||
|
organizationId: organization.id,
|
||||||
|
organization: {
|
||||||
|
id: organization.id,
|
||||||
|
isPlatform: organization.isPlatform,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: user
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: me,
|
data: me,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { IsInt, IsEmail, IsOptional, IsString } from "class-validator";
|
import { Type } from "class-transformer";
|
||||||
|
import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class MeOrgOutput {
|
||||||
|
isPlatform!: boolean;
|
||||||
|
|
||||||
|
id!: number;
|
||||||
|
}
|
||||||
export class MeOutput {
|
export class MeOutput {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id!: number;
|
id!: number;
|
||||||
@@ -25,4 +31,9 @@ export class MeOutput {
|
|||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
organizationId!: number | null;
|
organizationId!: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => MeOrgOutput)
|
||||||
|
organization?: MeOrgOutput;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsEnum } from "class-validator";
|
import { IsEnum, IsNumber, IsString } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class ProviderVerifyClientData {
|
||||||
|
@IsString()
|
||||||
|
clientId!: string;
|
||||||
|
@IsNumber()
|
||||||
|
organizationId!: number;
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
}
|
||||||
export class ProviderVerifyClientOutput {
|
export class ProviderVerifyClientOutput {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
data!: ProviderVerifyClientData;
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,11 @@ export class CalProviderController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
|
data: {
|
||||||
|
clientId: oAuthClient.id,
|
||||||
|
organizationId: oAuthClient.organizationId,
|
||||||
|
name: oAuthClient.name,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,7 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiResponse, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
|
||||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
@@ -36,7 +36,7 @@ import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input"
|
|||||||
version: VERSION_2024_04_15_VALUE,
|
version: VERSION_2024_04_15_VALUE,
|
||||||
})
|
})
|
||||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@DocsTags("Schedules")
|
@DocsExcludeController(true)
|
||||||
export class SchedulesController_2024_04_15 {
|
export class SchedulesController_2024_04_15 {
|
||||||
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
|
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
|
||||||
|
|
||||||
|
|||||||
+32
-2
@@ -17,7 +17,7 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
|
||||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
@@ -38,11 +38,38 @@ import {
|
|||||||
})
|
})
|
||||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@DocsTags("Schedules")
|
@DocsTags("Schedules")
|
||||||
|
@ApiHeader({
|
||||||
|
name: "cal-api-version",
|
||||||
|
description: `Must be set to \`2024-06-11\``,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@ApiHeader({
|
||||||
|
name: "Authorization",
|
||||||
|
description:
|
||||||
|
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
export class SchedulesController_2024_06_11 {
|
export class SchedulesController_2024_06_11 {
|
||||||
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
|
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Create a schedule",
|
||||||
|
description: `
|
||||||
|
The point of creating schedules is for event types to be available at specific times.
|
||||||
|
|
||||||
|
First goal of schedules is to have a default schedule. If you are platform customer and created managed users, then it is important to note that each managed user should have a default schedule.
|
||||||
|
1. If you passed \`timeZone\` when creating managed user, then the default schedule from Monday to Friday from 9AM to 5PM will be created with that timezone. Managed user can then change the default schedule via \`AvailabilitySettings\` atom.
|
||||||
|
2. If you did not, then we assume you want that user has specific schedule right away. You should create default schedule by specifying
|
||||||
|
\`"isDefault": true\` in the request body. Until the user has a default schedule that user can't be booked or manage his / her schedule via the AvailabilitySettings atom.
|
||||||
|
|
||||||
|
Second goal is to create other schedules that event types can point to, so that when that event is booked availability is not checked against the default schedule but against that specific schedule.
|
||||||
|
After creating a non default schedule you can update event type to point to that schedule via the PATCH \`event-types/{eventTypeId}\` endpoint.
|
||||||
|
|
||||||
|
When specifying start time and end time for each day use 24 hour format e.g. 08:00, 15:00 etc.
|
||||||
|
`,
|
||||||
|
})
|
||||||
async createSchedule(
|
async createSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: CreateScheduleInput_2024_06_11
|
@Body() bodySchedule: CreateScheduleInput_2024_06_11
|
||||||
@@ -59,7 +86,7 @@ export class SchedulesController_2024_06_11 {
|
|||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: "Returns the default schedule",
|
description: "Returns the default schedule of the authenticated user",
|
||||||
type: GetDefaultScheduleOutput_2024_06_11,
|
type: GetDefaultScheduleOutput_2024_06_11,
|
||||||
})
|
})
|
||||||
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
|
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
|
||||||
@@ -88,6 +115,9 @@ export class SchedulesController_2024_06_11 {
|
|||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Returns all schedules of the authenticated user",
|
||||||
|
})
|
||||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -1,9 +1,6 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import {
|
import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries";
|
||||||
transformApiScheduleOverrides,
|
|
||||||
transformApiScheduleAvailability,
|
|
||||||
} from "@calcom/platform-libraries-0.0.23";
|
|
||||||
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
|||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { TRPCError } from "@calcom/platform-libraries-0.0.23";
|
import { TRPCError } from "@calcom/platform-libraries";
|
||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Catch(TRPCError)
|
@Catch(TRPCError)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum AuthMethods {
|
||||||
|
"API_KEY" = "api-key",
|
||||||
|
"ACCESS_TOKEN" = "access-token",
|
||||||
|
"OAUTH_CLIENT" = "oauth-client",
|
||||||
|
"NEXT_AUTH" = "next-auth",
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||||
|
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||||
|
|
||||||
|
export const GetAuthMethod = createParamDecorator<unknown, ExecutionContext>((_data, ctx) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
const authMethod = request.authMethod as AuthMethods;
|
||||||
|
|
||||||
|
if (!authMethod) {
|
||||||
|
throw new Error("GetAuthMethod decorator : auth method not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
return authMethod;
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
@@ -12,13 +13,14 @@ export class OrganizationRolesGuard implements CanActivate {
|
|||||||
constructor(
|
constructor(
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private organizationsService: OrganizationsService,
|
private organizationsService: OrganizationsService,
|
||||||
private membershipRepository: MembershipsRepository
|
private membershipRepository: MembershipsRepository,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const user: UserWithProfile = request.user;
|
const user: UserWithProfile = request.user;
|
||||||
const organizationId = user?.movedToProfile?.organizationId || user?.organizationId;
|
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||||
|
|
||||||
if (!user || !organizationId) {
|
if (!user || !organizationId) {
|
||||||
throw new ForbiddenException("No organization associated with the user.");
|
throw new ForbiddenException("No organization associated with the user.");
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||||
|
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
type CachedData = {
|
||||||
|
org?: Team;
|
||||||
|
canAccess?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IsWebhookInOrg implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private organizationsRepository: OrganizationsRepository,
|
||||||
|
private organizationsWebhooksRepository: OrganizationsWebhooksRepository,
|
||||||
|
private readonly redisService: RedisService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
let canAccess = false;
|
||||||
|
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
|
||||||
|
const webhookId: string = request.params.webhookId;
|
||||||
|
const organizationId: string = request.params.orgId;
|
||||||
|
|
||||||
|
if (!organizationId) {
|
||||||
|
throw new ForbiddenException("No organization id found in request params.");
|
||||||
|
}
|
||||||
|
if (!webhookId) {
|
||||||
|
throw new ForbiddenException("No webhook id found in request params.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const REDIS_CACHE_KEY = `apiv2:org:${webhookId}:guard:isWebhookInOrg`;
|
||||||
|
const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY);
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData;
|
||||||
|
if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) {
|
||||||
|
request.organization = cachedOrg;
|
||||||
|
return cachedCanAccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const org = await this.organizationsRepository.findById(Number(organizationId));
|
||||||
|
|
||||||
|
if (org?.isOrganization) {
|
||||||
|
const isWebhookInOrg = await this.organizationsWebhooksRepository.findWebhook(
|
||||||
|
Number(organizationId),
|
||||||
|
webhookId
|
||||||
|
);
|
||||||
|
if (isWebhookInOrg) canAccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org) {
|
||||||
|
await this.redisService.redis.set(
|
||||||
|
REDIS_CACHE_KEY,
|
||||||
|
JSON.stringify({ org: org, canAccess } satisfies CachedData),
|
||||||
|
"EX",
|
||||||
|
300
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canAccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
|||||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
import { hasPermissions } from "@calcom/platform-utils";
|
import { hasPermissions } from "@calcom/platform-utils";
|
||||||
|
|
||||||
@@ -24,6 +25,12 @@ export class PermissionsGuard implements CanActivate {
|
|||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||||
|
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||||
|
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
|
||||||
|
|
||||||
|
if (nextAuthToken) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!authString) {
|
if (!authString) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
|
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
@@ -11,6 +12,7 @@ import { Injectable, InternalServerErrorException, UnauthorizedException } from
|
|||||||
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 { 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";
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticate(request: Request) {
|
async authenticate(request: Request & { authMethod: AuthMethods }) {
|
||||||
try {
|
try {
|
||||||
const { params } = request;
|
const { params } = request;
|
||||||
const oAuthClientSecret = request.get(X_CAL_SECRET_KEY);
|
const oAuthClientSecret = request.get(X_CAL_SECRET_KEY);
|
||||||
@@ -37,14 +39,26 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
const bearerToken = request.get("Authorization")?.replace("Bearer ", "");
|
const bearerToken = request.get("Authorization")?.replace("Bearer ", "");
|
||||||
|
|
||||||
if (oAuthClientId && oAuthClientSecret) {
|
if (oAuthClientId && oAuthClientSecret) {
|
||||||
|
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
||||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret);
|
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bearerToken) {
|
if (bearerToken) {
|
||||||
const requestOrigin = request.get("Origin");
|
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, requestOrigin);
|
return await this.authenticateBearerToken(bearerToken, requestOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||||
|
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
|
||||||
|
|
||||||
|
if (nextAuthToken) {
|
||||||
|
request.authMethod = AuthMethods["NEXT_AUTH"];
|
||||||
|
return await this.authenticateNextAuth(nextAuthToken);
|
||||||
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException(
|
||||||
"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"
|
||||||
);
|
);
|
||||||
@@ -58,6 +72,11 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async authenticateNextAuth(token: { email?: string | null }) {
|
||||||
|
const user = await this.nextAuthStrategy(token);
|
||||||
|
return this.success(user);
|
||||||
|
}
|
||||||
|
|
||||||
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
||||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
||||||
return this.success(user);
|
return this.success(user);
|
||||||
@@ -163,4 +182,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async nextAuthStrategy(token: { email?: string | null }) {
|
||||||
|
if (!token.email) {
|
||||||
|
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userRepository.findByEmailWithProfile(token.email);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
|||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { BullModule } from "@nestjs/bull";
|
import { BullModule } from "@nestjs/bull";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
|
|||||||
duration: 1000,
|
duration: 1000,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
UsersModule,
|
||||||
],
|
],
|
||||||
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
|
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
|
||||||
exports: [BillingService, BillingRepository],
|
exports: [BillingService, BillingRepository],
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
|||||||
export class CredentialsRepository {
|
export class CredentialsRepository {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async createAppCredential(
|
async upsertAppCredential(
|
||||||
type: keyof typeof APPS_TYPE_ID_MAPPING,
|
type: keyof typeof APPS_TYPE_ID_MAPPING,
|
||||||
key: Prisma.InputJsonValue,
|
key: Prisma.InputJsonValue,
|
||||||
userId: number
|
userId: number,
|
||||||
|
credentialId?: number | null
|
||||||
) {
|
) {
|
||||||
const credential = await this.getByTypeAndUserId(type, userId);
|
|
||||||
return this.dbWrite.prisma.credential.upsert({
|
return this.dbWrite.prisma.credential.upsert({
|
||||||
create: {
|
create: {
|
||||||
type,
|
type,
|
||||||
@@ -27,7 +27,7 @@ export class CredentialsRepository {
|
|||||||
invalid: false,
|
invalid: false,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
id: credential?.id ?? 0,
|
id: credentialId ?? 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,10 @@ export class CredentialsRepository {
|
|||||||
return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } });
|
return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllUserCredentialsByTypeAndId(type: string, userId: number) {
|
||||||
|
return this.dbRead.prisma.credential.findMany({ where: { type, userId } });
|
||||||
|
}
|
||||||
|
|
||||||
getUserCredentialsByIds(userId: number, credentialIds: number[]) {
|
getUserCredentialsByIds(userId: number, credentialIds: number[]) {
|
||||||
return this.dbRead.prisma.credential.findMany({
|
return this.dbRead.prisma.credential.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -60,6 +64,30 @@ export class CredentialsRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserCredentialById(userId: number, credentialId: number, type: string) {
|
||||||
|
return await this.dbRead.prisma.credential.findUnique({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
id: credentialId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
type: true,
|
||||||
|
key: true,
|
||||||
|
userId: true,
|
||||||
|
teamId: true,
|
||||||
|
appId: true,
|
||||||
|
invalid: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CredentialsWithUserEmail = Awaited<
|
export type CredentialsWithUserEmail = Awaited<
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class DeploymentsService {
|
|||||||
if (!licenseKey) {
|
if (!licenseKey) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const licenseKeyUrl = this.configService.get("api.licenseKeyUrl") + `?key=${licenseKey}`;
|
const licenseKeyUrl = this.configService.get("api.licenseKeyUrl") + `/${licenseKey}`;
|
||||||
const cachedData = await this.redisService.redis.get(getLicenseCacheKey(licenseKey));
|
const cachedData = await this.redisService.redis.get(getLicenseCacheKey(licenseKey));
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
return (JSON.parse(cachedData) as LicenseCheckResponse)?.valid;
|
return (JSON.parse(cachedData) as LicenseCheckResponse)?.valid;
|
||||||
|
|||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { DestinationCalendarsOutputResponseDto } from "@/modules/destination-calendars/outputs/destination-calendars.output";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { PlatformOAuthClient, Team, User, Credential } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
|
||||||
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
|
||||||
|
import { APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||||
|
|
||||||
|
describe("Platform Destination Calendar Endpoints", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let organization: Team;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||||
|
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
|
||||||
|
let appleCalendarCredentials: Credential;
|
||||||
|
let user: User;
|
||||||
|
let accessTokenSecret: string;
|
||||||
|
let refreshTokenSecret: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||||
|
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
|
||||||
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
|
||||||
|
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
|
||||||
|
accessTokenSecret = tokens.accessToken;
|
||||||
|
refreshTokenSecret = tokens.refreshToken;
|
||||||
|
appleCalendarCredentials = await credentialsRepositoryFixture.create(
|
||||||
|
APPLE_CALENDAR_TYPE,
|
||||||
|
{},
|
||||||
|
user.id,
|
||||||
|
APPLE_CALENDAR_ID
|
||||||
|
);
|
||||||
|
jest.spyOn(CalendarsService.prototype, "getCalendars").mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
connectedCalendars: [
|
||||||
|
{
|
||||||
|
integration: {
|
||||||
|
installed: false,
|
||||||
|
type: "apple_calendar",
|
||||||
|
title: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
variant: "calendar",
|
||||||
|
slug: "",
|
||||||
|
locationOption: null,
|
||||||
|
categories: ["calendar"],
|
||||||
|
logo: "",
|
||||||
|
publisher: "",
|
||||||
|
url: "",
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
calendars: {
|
||||||
|
externalId:
|
||||||
|
"https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||||
|
readOnly: false,
|
||||||
|
integration: "apple_calendar",
|
||||||
|
credentialId: appleCalendarCredentials.id,
|
||||||
|
primary: true,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
error: { message: "" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: [CLIENT_REDIRECT_URI],
|
||||||
|
permissions: 32,
|
||||||
|
};
|
||||||
|
const secret = "secret";
|
||||||
|
|
||||||
|
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(oAuthClient).toBeDefined();
|
||||||
|
expect(accessTokenSecret).toBeDefined();
|
||||||
|
expect(refreshTokenSecret).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`POST /v2/destination-calendars: it should respond with a 200 returning back the user updated destination calendar`, async () => {
|
||||||
|
const body = {
|
||||||
|
integration: appleCalendarCredentials.type,
|
||||||
|
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.put("/v2/destination-calendars")
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: DestinationCalendarsOutputResponseDto = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.credentialId).toEqual(appleCalendarCredentials.id);
|
||||||
|
expect(responseBody.data.integration).toEqual(body.integration);
|
||||||
|
expect(responseBody.data.externalId).toEqual(body.externalId);
|
||||||
|
expect(responseBody.data.userId).toEqual(user.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`POST /v2/destination-calendars: should fail 400 if calendar type is invalid`, async () => {
|
||||||
|
const body = {
|
||||||
|
integration: "not-supported-calendar",
|
||||||
|
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.put("/v2/destination-calendars")
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.send(body)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { DestinationCalendarsInputBodyDto } from "@/modules/destination-calendars/inputs/destination-calendars.input";
|
||||||
|
import {
|
||||||
|
DestinationCalendarsOutputDto,
|
||||||
|
DestinationCalendarsOutputResponseDto,
|
||||||
|
} from "@/modules/destination-calendars/outputs/destination-calendars.output";
|
||||||
|
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Body, Controller, Put, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/destination-calendars",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Destination-Calendars")
|
||||||
|
export class DestinationCalendarsController {
|
||||||
|
constructor(private readonly destinationCalendarsService: DestinationCalendarsService) {}
|
||||||
|
|
||||||
|
@Put("/")
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@DocsTags("Select a third party destination calendar where events will be created")
|
||||||
|
async updateDestinationCalendars(
|
||||||
|
@Body() input: DestinationCalendarsInputBodyDto,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<DestinationCalendarsOutputResponseDto> {
|
||||||
|
const { integration, externalId } = input;
|
||||||
|
const updatedDestinationCalendar = await this.destinationCalendarsService.updateDestinationCalendars(
|
||||||
|
integration,
|
||||||
|
externalId,
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(DestinationCalendarsOutputDto, updatedDestinationCalendar, {
|
||||||
|
strategy: "excludeAll",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 1537 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user