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 |
+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?
|
|
||||||
@@ -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
|
||||||
@@ -4,6 +4,11 @@ on:
|
|||||||
delete:
|
delete:
|
||||||
branches-ignore: [main, gh-pages]
|
branches-ignore: [main, gh-pages]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
# ensures that currently running Playwright workflow of deleted branch gets cancelled
|
# ensures that currently running Playwright workflow of deleted branch gets cancelled
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.event.ref }}
|
group: ${{ github.event.ref }}
|
||||||
@@ -20,7 +25,9 @@ jobs:
|
|||||||
- name: Checkout GitHub Pages Branch
|
- name: Checkout GitHub Pages Branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
repository: calcom/test-results
|
||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
- name: Set Git User
|
- name: Set Git User
|
||||||
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -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: >-
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
name: PR Reviewed
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label-pr:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
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.READY_FOR_E2E_PAT }}
|
|
||||||
labels: 'ready-for-e2e'
|
|
||||||
@@ -132,6 +132,13 @@ 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
|
||||||
|
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, deps]
|
needs: [changes, check-label, deps]
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ jobs:
|
|||||||
- name: Checkout GitHub Pages Branch
|
- name: Checkout GitHub Pages Branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
repository: calcom/test-results
|
||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
- name: Set Git User
|
- name: Set Git User
|
||||||
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||||
run: |
|
run: |
|
||||||
@@ -65,7 +67,7 @@ jobs:
|
|||||||
- name: Output Report URL as Workflow Annotation
|
- name: Output Report URL as Workflow Annotation
|
||||||
id: url
|
id: url
|
||||||
run: |
|
run: |
|
||||||
FULL_HTML_REPORT_URL=https://calcom.github.io/cal.com/$HTML_REPORT_URL_PATH
|
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 "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
||||||
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}:
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,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
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,13 +20,15 @@
|
|||||||
"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-libraries": "npm:@calcom/platform-libraries@0.0.28",
|
"@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-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
|
|||||||
@@ -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,6 +14,7 @@ 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";
|
||||||
@@ -21,7 +22,7 @@ import { withApiAuth } from "test/utils/withApiAuth";
|
|||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||||
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,6 +50,7 @@ import {
|
|||||||
getBookingInfo,
|
getBookingInfo,
|
||||||
handleCancelBooking,
|
handleCancelBooking,
|
||||||
getBookingForReschedule,
|
getBookingForReschedule,
|
||||||
|
ErrorCode,
|
||||||
} from "@calcom/platform-libraries";
|
} 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";
|
||||||
@@ -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,5 +1,5 @@
|
|||||||
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";
|
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@@ -21,4 +21,8 @@ export class CreateRecurringBookingInput extends CreateBookingInput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
currentRecurringIndex?: number;
|
currentRecurringIndex?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
recurringEventId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
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,9 +16,11 @@ 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 { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
||||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
|
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+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,6 +1,7 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -9,7 +10,11 @@ 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,
|
||||||
|
|||||||
+18
-6
@@ -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,6 +15,7 @@ import {
|
|||||||
updateEventType,
|
updateEventType,
|
||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
getEventTypesPublic,
|
getEventTypesPublic,
|
||||||
|
systemBeforeFieldEmail,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
import { EventType } from "@calcom/prisma/client";
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-5
@@ -20,13 +20,13 @@ 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 { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/dist/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
|
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
describe("Event types Endpoints", () => {
|
describe("Event types Endpoints", () => {
|
||||||
@@ -230,6 +230,11 @@ describe("Event types Endpoints", () => {
|
|||||||
value: 30,
|
value: 30,
|
||||||
rolling: true,
|
rolling: true,
|
||||||
},
|
},
|
||||||
|
recurrence: {
|
||||||
|
frequency: FrequencyInput.weekly,
|
||||||
|
interval: 2,
|
||||||
|
occurrences: 10,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -245,7 +250,6 @@ 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.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||||
@@ -253,6 +257,17 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||||
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
|
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
|
||||||
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
|
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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -278,6 +293,11 @@ describe("Event types Endpoints", () => {
|
|||||||
value: 40,
|
value: 40,
|
||||||
rolling: false,
|
rolling: false,
|
||||||
},
|
},
|
||||||
|
recurrence: {
|
||||||
|
frequency: FrequencyInput.monthly,
|
||||||
|
interval: 4,
|
||||||
|
occurrences: 10,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -303,6 +323,7 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||||
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
|
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
|
||||||
expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow);
|
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;
|
||||||
@@ -311,6 +332,7 @@ describe("Event types Endpoints", () => {
|
|||||||
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
|
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
|
||||||
eventType.offsetStart = updatedEventType.offsetStart;
|
eventType.offsetStart = updatedEventType.offsetStart;
|
||||||
eventType.bookingWindow = updatedEventType.bookingWindow;
|
eventType.bookingWindow = updatedEventType.bookingWindow;
|
||||||
|
eventType.recurrence = updatedEventType.recurrence;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -351,6 +373,7 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
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 () => {
|
||||||
@@ -381,6 +404,7 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
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 () => {
|
||||||
@@ -406,6 +430,7 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||||
|
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/:id not existing`, async () => {
|
it(`/GET/:id not existing`, async () => {
|
||||||
@@ -460,6 +485,16 @@ describe("Event types Endpoints", () => {
|
|||||||
let legacyEventTypeId1: number;
|
let legacyEventTypeId1: number;
|
||||||
let legacyEventTypeId2: 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 () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
@@ -529,7 +564,7 @@ describe("Event types Endpoints", () => {
|
|||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty bookingFields if system fields are the only one in database", async () => {
|
it("should return system bookingFields stored in database", async () => {
|
||||||
const legacyEventTypeInput = {
|
const legacyEventTypeInput = {
|
||||||
title: "legacy event type",
|
title: "legacy event type",
|
||||||
description: "legacy event type description",
|
description: "legacy event type description",
|
||||||
@@ -622,11 +657,11 @@ describe("Event types Endpoints", () => {
|
|||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
const fetchedEventType = responseBody.data;
|
const fetchedEventType = responseBody.data;
|
||||||
expect(fetchedEventType.bookingFields).toEqual([]);
|
expect(fetchedEventType.bookingFields).toEqual(expectedReturnSystemFields);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return user created bookingFields among system fields in the database", async () => {
|
it("should return user created bookingFields with system fields", async () => {
|
||||||
const userDefinedBookingField = {
|
const userDefinedBookingField = {
|
||||||
name: "team",
|
name: "team",
|
||||||
type: "textarea",
|
type: "textarea",
|
||||||
@@ -739,7 +774,9 @@ describe("Event types Endpoints", () => {
|
|||||||
const fetchedEventType = responseBody.data;
|
const fetchedEventType = responseBody.data;
|
||||||
|
|
||||||
expect(fetchedEventType.bookingFields).toEqual([
|
expect(fetchedEventType.bookingFields).toEqual([
|
||||||
|
...expectedReturnSystemFields,
|
||||||
{
|
{
|
||||||
|
isDefault: false,
|
||||||
type: userDefinedBookingField.type,
|
type: userDefinedBookingField.type,
|
||||||
slug: userDefinedBookingField.name,
|
slug: userDefinedBookingField.name,
|
||||||
label: userDefinedBookingField.label,
|
label: userDefinedBookingField.label,
|
||||||
|
|||||||
+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,12 +1,13 @@
|
|||||||
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";
|
} 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";
|
||||||
@@ -25,16 +26,21 @@ type InputEventTransformed = Omit<
|
|||||||
| "periodCountCalendarDays"
|
| "periodCountCalendarDays"
|
||||||
| "periodStartDate"
|
| "periodStartDate"
|
||||||
| "periodEndDate"
|
| "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({
|
||||||
@@ -80,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;
|
||||||
|
}
|
||||||
+6
-4
@@ -46,6 +46,7 @@ export class EventTypesService_2024_06_14 {
|
|||||||
periodCountCalendarDays = undefined,
|
periodCountCalendarDays = undefined,
|
||||||
periodStartDate = undefined,
|
periodStartDate = undefined,
|
||||||
periodEndDate = undefined,
|
periodEndDate = undefined,
|
||||||
|
recurrence = undefined,
|
||||||
...bodyTransformed
|
...bodyTransformed
|
||||||
} = this.inputEventTypesService.transformInputCreateEventType(body);
|
} = this.inputEventTypesService.transformInputCreateEventType(body);
|
||||||
const { eventType: eventTypeCreated } = await createEventType({
|
const { eventType: eventTypeCreated } = await createEventType({
|
||||||
@@ -68,6 +69,7 @@ export class EventTypesService_2024_06_14 {
|
|||||||
periodCountCalendarDays,
|
periodCountCalendarDays,
|
||||||
periodStartDate,
|
periodStartDate,
|
||||||
periodEndDate,
|
periodEndDate,
|
||||||
|
recurrence,
|
||||||
...bodyTransformed,
|
...bodyTransformed,
|
||||||
},
|
},
|
||||||
ctx: {
|
ctx: {
|
||||||
@@ -119,11 +121,11 @@ 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);
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -159,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)
|
||||||
@@ -270,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 };
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-11
@@ -1,11 +1,16 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
transformApiEventTypeBookingFields,
|
transformBookingFieldsApiToInternal,
|
||||||
transformApiEventTypeLocations,
|
transformLocationsApiToInternal,
|
||||||
transformApiEventTypeIntervalLimits,
|
transformIntervalLimitsApiToInternal,
|
||||||
transformApiEventTypeFutureBookingLimits,
|
transformFutureBookingLimitsApiToInternal,
|
||||||
|
transformRecurrenceApiToInternal,
|
||||||
|
systemBeforeFieldName,
|
||||||
|
systemBeforeFieldEmail,
|
||||||
|
systemAfterFieldRescheduleReason,
|
||||||
} from "@calcom/platform-libraries";
|
} 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()
|
||||||
@@ -25,19 +30,22 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
bookingLimitsCount,
|
bookingLimitsCount,
|
||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
|
recurrence,
|
||||||
...rest
|
...rest
|
||||||
} = inputEventType;
|
} = 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,
|
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||||
durationLimits: bookingLimitsDuration
|
durationLimits: bookingLimitsDuration
|
||||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||||
: undefined,
|
: undefined,
|
||||||
...this.transformInputBookingWindow(bookingWindow),
|
...this.transformInputBookingWindow(bookingWindow),
|
||||||
|
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return eventType;
|
return eventType;
|
||||||
@@ -52,39 +60,61 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
bookingLimitsCount,
|
bookingLimitsCount,
|
||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
|
recurrence,
|
||||||
...rest
|
...rest
|
||||||
} = inputEventType;
|
} = 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,
|
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||||
durationLimits: bookingLimitsDuration
|
durationLimits: bookingLimitsDuration
|
||||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||||
: undefined,
|
: undefined,
|
||||||
...this.transformInputBookingWindow(bookingWindow),
|
...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"]) {
|
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
|
||||||
return transformApiEventTypeIntervalLimits(inputBookingFields);
|
return transformIntervalLimitsApiToInternal(inputBookingFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
|
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
|
||||||
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
|
const res = transformFutureBookingLimitsApiToInternal(inputBookingWindow);
|
||||||
return !!res ? res : {};
|
return !!res ? res : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
|
||||||
|
return transformRecurrenceApiToInternal(recurrence);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-15
@@ -4,16 +4,17 @@ import type { EventType, User, Schedule } from "@prisma/client";
|
|||||||
import {
|
import {
|
||||||
EventTypeMetaDataSchema,
|
EventTypeMetaDataSchema,
|
||||||
userMetadata,
|
userMetadata,
|
||||||
getResponseEventTypeLocations,
|
transformLocationsInternalToApi,
|
||||||
getResponseEventTypeBookingFields,
|
transformBookingFieldsInternalToApi,
|
||||||
parseRecurringEvent,
|
parseRecurringEvent,
|
||||||
TransformedLocationsSchema,
|
TransformedLocationsSchema,
|
||||||
BookingFieldsSchema,
|
BookingFieldsSchema,
|
||||||
SystemField,
|
SystemField,
|
||||||
UserField,
|
CustomField,
|
||||||
parseBookingLimit,
|
parseBookingLimit,
|
||||||
getResponseEventTypeIntervalLimits,
|
transformIntervalLimitsInternalToApi,
|
||||||
getResponseEventTypeFutureBookingLimits,
|
transformFutureBookingLimitsInternalToApi,
|
||||||
|
transformRecurrenceInternalToApi,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
|
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
const bookingFields = databaseEventType.bookingFields
|
const bookingFields = databaseEventType.bookingFields
|
||||||
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
|
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
|
||||||
: [];
|
: [];
|
||||||
const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
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 bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits);
|
||||||
@@ -114,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,
|
||||||
@@ -143,18 +144,20 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
|
|
||||||
transformLocations(locations: any) {
|
transformLocations(locations: any) {
|
||||||
if (!locations) return [];
|
if (!locations) return [];
|
||||||
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
|
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
|
||||||
}
|
}
|
||||||
|
|
||||||
transformBookingFields(inputBookingFields: (SystemField | UserField)[] | null) {
|
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
|
||||||
if (!inputBookingFields) return [];
|
if (!bookingFields) return [];
|
||||||
const userFields = inputBookingFields.filter((field) => field.editable === "user") as UserField[];
|
|
||||||
return getResponseEventTypeBookingFields(userFields);
|
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) {
|
||||||
@@ -180,10 +183,10 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
|
|
||||||
transformIntervalLimits(bookingLimits: any) {
|
transformIntervalLimits(bookingLimits: any) {
|
||||||
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
|
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
|
||||||
return getResponseEventTypeIntervalLimits(bookingLimitsParsed);
|
return transformIntervalLimitsInternalToApi(bookingLimitsParsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
|
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
|
||||||
return getResponseEventTypeFutureBookingLimits(bookingLimits);
|
return transformFutureBookingLimitsInternalToApi(bookingLimits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -30,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);
|
||||||
@@ -38,11 +39,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +55,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
|
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
|
||||||
|
|
||||||
if (nextAuthToken) {
|
if (nextAuthToken) {
|
||||||
|
request.authMethod = AuthMethods["NEXT_AUTH"];
|
||||||
return await this.authenticateNextAuth(nextAuthToken);
|
return await this.authenticateNextAuth(nextAuthToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { DestinationCalendarsController } from "@/modules/destination-calendars/
|
|||||||
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
|
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
|
||||||
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
|
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ import { Module } from "@nestjs/common";
|
|||||||
UsersRepository,
|
UsersRepository,
|
||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
|
SelectedCalendarsRepository,
|
||||||
],
|
],
|
||||||
controllers: [DestinationCalendarsController],
|
controllers: [DestinationCalendarsController],
|
||||||
exports: [DestinationCalendarsRepository],
|
exports: [DestinationCalendarsRepository],
|
||||||
|
|||||||
-1
@@ -79,7 +79,6 @@ export class OAuthClientUsersController {
|
|||||||
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
||||||
);
|
);
|
||||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||||
console.log("asap createUser client", JSON.stringify(client, null, 2));
|
|
||||||
|
|
||||||
const isPlatformManaged = true;
|
const isPlatformManaged = true;
|
||||||
const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser(
|
const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser(
|
||||||
|
|||||||
+5
-3
@@ -13,6 +13,7 @@ import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-gu
|
|||||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -59,7 +60,8 @@ export class OAuthClientsController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly oauthClientRepository: OAuthClientRepository,
|
private readonly oauthClientRepository: OAuthClientRepository,
|
||||||
private readonly userRepository: UsersRepository,
|
private readonly userRepository: UsersRepository,
|
||||||
private readonly teamsRepository: OrganizationsRepository
|
private readonly teamsRepository: OrganizationsRepository,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -74,7 +76,7 @@ export class OAuthClientsController {
|
|||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() body: CreateOAuthClientInput
|
@Body() body: CreateOAuthClientInput
|
||||||
): Promise<CreateOAuthClientResponseDto> {
|
): Promise<CreateOAuthClientResponseDto> {
|
||||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||||
);
|
);
|
||||||
@@ -100,7 +102,7 @@ export class OAuthClientsController {
|
|||||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||||
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
||||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||||
|
|
||||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||||
return { status: SUCCESS_STATUS, data: clients };
|
return { status: SUCCESS_STATUS, data: clients };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
CanActivate,
|
CanActivate,
|
||||||
@@ -11,12 +12,12 @@ import { Request } from "express";
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuthClientGuard implements CanActivate {
|
export class OAuthClientGuard implements CanActivate {
|
||||||
constructor(private oAuthClientRepository: OAuthClientRepository) {}
|
constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest<Request & { user: GetUserReturnType }>();
|
const request = context.switchToHttp().getRequest<Request & { user: GetUserReturnType }>();
|
||||||
const user: GetUserReturnType = request.user;
|
const user: GetUserReturnType = request.user;
|
||||||
const organizationId = user?.movedToProfile?.organizationId || user?.organizationId;
|
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||||
const oAuthClientId = request.params.clientId;
|
const oAuthClientId = request.params.clientId;
|
||||||
|
|
||||||
if (!oAuthClientId) {
|
if (!oAuthClientId) {
|
||||||
|
|||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
|
||||||
|
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||||
|
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { User, Team, Membership } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Organizations Attributes Options Endpoints", () => {
|
||||||
|
describe("User lacks required role", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let membershipFixtures: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "member@attributes-options-api.com";
|
||||||
|
let user: User;
|
||||||
|
let org: Team;
|
||||||
|
let membership: Membership;
|
||||||
|
let attributeId: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: "AttributesOptionsCorp",
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
organization: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true);
|
||||||
|
|
||||||
|
// Create an attribute for testing
|
||||||
|
attributeId = "test-attribute-id";
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to create attribute option", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
|
||||||
|
.send({
|
||||||
|
name: "Option 1",
|
||||||
|
value: "option1",
|
||||||
|
})
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to delete attribute option", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/v2/organizations/${org.id}/attributes/${attributeId}/options/1`)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await membershipFixtures.delete(membership.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("User has required role", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let membershipFixtures: MembershipRepositoryFixture;
|
||||||
|
let attributeRepositoryFixture: AttributeRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "admin@attributes-options-api.com";
|
||||||
|
let user: User;
|
||||||
|
let org: Team;
|
||||||
|
let membership: Membership;
|
||||||
|
let attributeId: string;
|
||||||
|
let createdOption: any;
|
||||||
|
|
||||||
|
const createOptionInput: CreateOrganizationAttributeOptionInput = {
|
||||||
|
value: "option1",
|
||||||
|
slug: "option1",
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
attributeRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: "AttributesOptionsCorp",
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
organization: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
|
||||||
|
|
||||||
|
// Create an attribute for testing
|
||||||
|
const attribute = await attributeRepositoryFixture.create({
|
||||||
|
name: "Test Attribute",
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "TEXT",
|
||||||
|
slug: "test-attribute",
|
||||||
|
});
|
||||||
|
attributeId = attribute.id;
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create attribute option", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
|
||||||
|
.send(createOptionInput)
|
||||||
|
.expect(201)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
createdOption = response.body.data;
|
||||||
|
expect(createdOption.value).toEqual(createOptionInput.value);
|
||||||
|
expect(createdOption.slug).toEqual(createOptionInput.slug);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get attribute options", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const options = response.body.data;
|
||||||
|
expect(options.length).toEqual(1);
|
||||||
|
expect(options[0].value).toEqual(createOptionInput.value);
|
||||||
|
expect(options[0].slug).toEqual(createOptionInput.slug);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update attribute option", async () => {
|
||||||
|
const updateOptionInput: UpdateOrganizationAttributeOptionInput = {
|
||||||
|
value: "updated-option-value",
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/v2/organizations/${org.id}/attributes/${attributeId}/options/${createdOption.id}`)
|
||||||
|
.send(updateOptionInput)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const updatedOption = response.body.data;
|
||||||
|
expect(updatedOption.value).toEqual(updateOptionInput.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should assign attribute option to user", async () => {
|
||||||
|
const assignInput: AssignOrganizationAttributeOptionToUserInput = {
|
||||||
|
attributeId: attributeId,
|
||||||
|
attributeOptionId: createdOption.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/attributes/options/${user.id}`)
|
||||||
|
.send(assignInput)
|
||||||
|
.expect(201)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get attribute options for user", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/attributes/options/${user.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const userOptions = response.body.data;
|
||||||
|
expect(userOptions.length).toEqual(1);
|
||||||
|
expect(userOptions[0].id).toEqual(createdOption.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should unassign attribute option from user", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/v2/organizations/${org.id}/attributes/options/${user.id}/${createdOption.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should delete attribute option", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/v2/organizations/${org.id}/attributes/${attributeId}/options/${createdOption.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await membershipFixtures.delete(membership.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||||
|
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { User, Team, Membership } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Organizations Attributes Endpoints", () => {
|
||||||
|
describe("User lacks required role", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let membershipFixtures: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "member@attributes-api.com";
|
||||||
|
let user: User;
|
||||||
|
let org: Team;
|
||||||
|
let membership: Membership;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: "AttributesCorp",
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
organization: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true);
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to create attribute for org", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/attributes`)
|
||||||
|
.send({
|
||||||
|
key: "department",
|
||||||
|
value: "engineering",
|
||||||
|
})
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to delete attribute for org", async () => {
|
||||||
|
return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/attributes/1`).expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await membershipFixtures.delete(membership.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("User has required role", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let membershipFixtures: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "admin@attributes-api.com";
|
||||||
|
let user: User;
|
||||||
|
let org: Team;
|
||||||
|
let membership: Membership;
|
||||||
|
|
||||||
|
let createdAttribute: any;
|
||||||
|
|
||||||
|
const createAttributeInput: CreateOrganizationAttributeInput = {
|
||||||
|
name: "department",
|
||||||
|
slug: "department",
|
||||||
|
type: "TEXT",
|
||||||
|
options: [],
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: "AttributesCorp",
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
organization: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create attribute for org", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/attributes`)
|
||||||
|
.send(createAttributeInput)
|
||||||
|
.expect(201)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
createdAttribute = response.body.data;
|
||||||
|
expect(createdAttribute.type).toEqual(createAttributeInput.type);
|
||||||
|
expect(createdAttribute.slug).toEqual(createAttributeInput.slug);
|
||||||
|
expect(createdAttribute.enabled).toEqual(createAttributeInput.enabled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get org attributes", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/attributes`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const attributes = response.body.data;
|
||||||
|
expect(attributes.length).toEqual(1);
|
||||||
|
expect(attributes[0].name).toEqual(createAttributeInput.name);
|
||||||
|
expect(attributes[0].slug).toEqual(createAttributeInput.slug);
|
||||||
|
expect(attributes[0].type).toEqual(createAttributeInput.type);
|
||||||
|
expect(attributes[0].enabled).toEqual(createAttributeInput.enabled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get single org attribute", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const attribute = response.body.data;
|
||||||
|
expect(attribute.name).toEqual(createAttributeInput.name);
|
||||||
|
expect(attribute.slug).toEqual(createAttributeInput.slug);
|
||||||
|
expect(attribute.type).toEqual(createAttributeInput.type);
|
||||||
|
expect(attribute.enabled).toEqual(createAttributeInput.enabled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update org attribute", async () => {
|
||||||
|
const updateAttributeInput: UpdateOrganizationAttributeInput = {
|
||||||
|
name: "marketing",
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`)
|
||||||
|
.send(updateAttributeInput)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const updatedAttribute = response.body.data;
|
||||||
|
expect(updatedAttribute.name).toEqual(updateAttributeInput.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should delete org attribute", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await membershipFixtures.delete(membership.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
|
||||||
|
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||||
|
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||||
|
import {
|
||||||
|
AssignOptionUserOutput,
|
||||||
|
UnassignOptionUserOutput,
|
||||||
|
} from "@/modules/organizations/outputs/attributes/options/assign-option-user.output";
|
||||||
|
import { CreateAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/create-option.output";
|
||||||
|
import { DeleteAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/delete-option.output";
|
||||||
|
import { GetOptionUserOutput } from "@/modules/organizations/outputs/attributes/options/get-option-user.output";
|
||||||
|
import { GetAllAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/get-option.output";
|
||||||
|
import { UpdateAttributeOptionOutput } from "@/modules/organizations/outputs/attributes/options/update-option.output";
|
||||||
|
import { OrganizationAttributeOptionService } from "@/modules/organizations/services/attributes/organization-attributes-option.service";
|
||||||
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
export class OrganizationsOptionsAttributesController {
|
||||||
|
constructor(private readonly organizationsAttributesOptionsService: OrganizationAttributeOptionService) {}
|
||||||
|
|
||||||
|
// Creates an attribute option for an attribute
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/attributes/:attributeId/options")
|
||||||
|
async createOrganizationAttributeOption(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("attributeId") attributeId: string,
|
||||||
|
@Body() bodyAttribute: CreateOrganizationAttributeOptionInput
|
||||||
|
): Promise<CreateAttributeOptionOutput> {
|
||||||
|
const attributeOption =
|
||||||
|
await this.organizationsAttributesOptionsService.createOrganizationAttributeOption(
|
||||||
|
orgId,
|
||||||
|
attributeId,
|
||||||
|
bodyAttribute
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributeOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes an attribute option for an attribute
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Delete("/attributes/:attributeId/options/:optionId")
|
||||||
|
async deleteOrganizationAttributeOption(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("attributeId") attributeId: string,
|
||||||
|
@Param("optionId") optionId: string
|
||||||
|
): Promise<DeleteAttributeOptionOutput> {
|
||||||
|
const attributeOption =
|
||||||
|
await this.organizationsAttributesOptionsService.deleteOrganizationAttributeOption(
|
||||||
|
orgId,
|
||||||
|
attributeId,
|
||||||
|
optionId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributeOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates an attribute option for an attribute
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Patch("/attributes/:attributeId/options/:optionId")
|
||||||
|
async updateOrganizationAttributeOption(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("attributeId") attributeId: string,
|
||||||
|
@Param("optionId") optionId: string,
|
||||||
|
@Body() bodyAttribute: UpdateOrganizationAttributeOptionInput
|
||||||
|
): Promise<UpdateAttributeOptionOutput> {
|
||||||
|
const attributeOption =
|
||||||
|
await this.organizationsAttributesOptionsService.updateOrganizationAttributeOption(
|
||||||
|
orgId,
|
||||||
|
attributeId,
|
||||||
|
optionId,
|
||||||
|
bodyAttribute
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributeOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets all attribute options for an attribute
|
||||||
|
@Roles("ORG_MEMBER")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/attributes/:attributeId/options")
|
||||||
|
async getOrganizationAttributeOptions(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("attributeId") attributeId: string
|
||||||
|
): Promise<GetAllAttributeOptionOutput> {
|
||||||
|
const attributeOptions = await this.organizationsAttributesOptionsService.getOrganizationAttributeOptions(
|
||||||
|
orgId,
|
||||||
|
attributeId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributeOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign attribute option to user
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/attributes/options/:userId")
|
||||||
|
async assignOrganizationAttributeOptionToUser(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("userId", ParseIntPipe) userId: number,
|
||||||
|
@Body() bodyAttribute: AssignOrganizationAttributeOptionToUserInput
|
||||||
|
): Promise<AssignOptionUserOutput> {
|
||||||
|
const attributeOption =
|
||||||
|
await this.organizationsAttributesOptionsService.assignOrganizationAttributeOptionToUser(
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
bodyAttribute
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributeOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unassign attribute option from user
|
||||||
|
@Roles("ORG_MEMBER")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Delete("/attributes/options/:userId/:attributeOptionId")
|
||||||
|
async unassignOrganizationAttributeOptionFromUser(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("userId", ParseIntPipe) userId: number,
|
||||||
|
@Param("attributeOptionId") attributeOptionId: string
|
||||||
|
): Promise<UnassignOptionUserOutput> {
|
||||||
|
const attributeOption =
|
||||||
|
await this.organizationsAttributesOptionsService.unassignOrganizationAttributeOptionFromUser(
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
attributeOptionId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributeOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles("ORG_MEMBER")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/attributes/options/:userId")
|
||||||
|
async getOrganizationAttributeOptionsForUser(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("userId", ParseIntPipe) userId: number
|
||||||
|
): Promise<GetOptionUserOutput> {
|
||||||
|
const attributeOptions =
|
||||||
|
await this.organizationsAttributesOptionsService.getOrganizationAttributeOptionsForUser(orgId, userId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributeOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||||
|
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||||
|
import { CreateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/create-organization-attributes.output";
|
||||||
|
import { DeleteOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/delete-organization-attributes.output";
|
||||||
|
import {
|
||||||
|
GetOrganizationAttributesOutput,
|
||||||
|
GetSingleAttributeOutput,
|
||||||
|
} from "@/modules/organizations/outputs/attributes/get-organization-attributes.output";
|
||||||
|
import { UpdateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/update-organization-attributes.output";
|
||||||
|
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
export class OrganizationsAttributesController {
|
||||||
|
constructor(private readonly organizationsAttributesService: OrganizationAttributesService) {}
|
||||||
|
// Gets all attributes for an organization
|
||||||
|
@Roles("ORG_MEMBER")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/attributes")
|
||||||
|
async getOrganizationAttributes(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Query() queryParams: SkipTakePagination
|
||||||
|
): Promise<GetOrganizationAttributesOutput> {
|
||||||
|
const { skip, take } = queryParams;
|
||||||
|
const attributes = await this.organizationsAttributesService.getOrganizationAttributes(orgId, skip, take);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a single attribute for an organization
|
||||||
|
@Roles("ORG_MEMBER")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/attributes/:attributeId")
|
||||||
|
async getOrganizationAttribute(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("attributeId") attributeId: string
|
||||||
|
): Promise<GetSingleAttributeOutput> {
|
||||||
|
const attribute = await this.organizationsAttributesService.getOrganizationAttribute(orgId, attributeId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attribute,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an attribute for an organization
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/attributes")
|
||||||
|
async createOrganizationAttribute(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Body() bodyAttribute: CreateOrganizationAttributeInput
|
||||||
|
): Promise<CreateOrganizationAttributesOutput> {
|
||||||
|
const attribute = await this.organizationsAttributesService.createOrganizationAttribute(
|
||||||
|
orgId,
|
||||||
|
bodyAttribute
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attribute,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates an attribute for an organization
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Patch("/attributes/:attributeId")
|
||||||
|
async updateOrganizationAttribute(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("attributeId") attributeId: string,
|
||||||
|
@Body() bodyAttribute: UpdateOrganizationAttributeInput
|
||||||
|
): Promise<UpdateOrganizationAttributesOutput> {
|
||||||
|
const attribute = await this.organizationsAttributesService.updateOrganizationAttribute(
|
||||||
|
orgId,
|
||||||
|
attributeId,
|
||||||
|
bodyAttribute
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attribute,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes an attribute for an organization
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Delete("/attributes/:attributeId")
|
||||||
|
async deleteOrganizationAttribute(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("attributeId") attributeId: string
|
||||||
|
): Promise<DeleteOrganizationAttributesOutput> {
|
||||||
|
const attribute = await this.organizationsAttributesService.deleteOrganizationAttribute(
|
||||||
|
orgId,
|
||||||
|
attributeId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: attribute,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-1
@@ -1,3 +1,5 @@
|
|||||||
|
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
|
||||||
|
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
|
||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
|
||||||
import {
|
import {
|
||||||
CreateTeamEventTypeInput_2024_06_14,
|
CreateTeamEventTypeInput_2024_06_14,
|
||||||
GetTeamEventTypesQuery_2024_06_14,
|
GetTeamEventTypesQuery_2024_06_14,
|
||||||
@@ -91,6 +94,30 @@ export class OrganizationsEventTypesController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Post("/teams/:teamId/event-types/:eventTypeId/create-phone-call")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, RolesGuard)
|
||||||
|
async createPhoneCall(
|
||||||
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Body() body: CreatePhoneCallInput,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<CreatePhoneCallOutput> {
|
||||||
|
const data = await handleCreatePhoneCall({
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
profile: { organization: { id: orgId } },
|
||||||
|
},
|
||||||
|
input: { ...body, eventTypeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
|
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
|
||||||
@Get("/teams/:teamId/event-types")
|
@Get("/teams/:teamId/event-types")
|
||||||
async getTeamEventTypes(
|
async getTeamEventTypes(
|
||||||
@@ -138,7 +165,7 @@ export class OrganizationsEventTypesController {
|
|||||||
@Patch("/teams/:teamId/event-types/:eventTypeId")
|
@Patch("/teams/:teamId/event-types/:eventTypeId")
|
||||||
async updateTeamEventType(
|
async updateTeamEventType(
|
||||||
@Param("teamId", ParseIntPipe) teamId: number,
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
@Param("eventTypeId") eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14
|
@Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14
|
||||||
): Promise<UpdateTeamEventTypeOutput> {
|
): Promise<UpdateTeamEventTypeOutput> {
|
||||||
|
|||||||
+1
-1
@@ -17,6 +17,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
|
||||||
import {
|
import {
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
CreateTeamEventTypeInput_2024_06_14,
|
CreateTeamEventTypeInput_2024_06_14,
|
||||||
@@ -24,7 +25,6 @@ import {
|
|||||||
TeamEventTypeOutput_2024_06_14,
|
TeamEventTypeOutput_2024_06_14,
|
||||||
UpdateTeamEventTypeInput_2024_06_14,
|
UpdateTeamEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
|
|
||||||
import { Team } from "@calcom/prisma/client";
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Organizations Event Types Endpoints", () => {
|
describe("Organizations Event Types Endpoints", () => {
|
||||||
|
|||||||
+5
-3
@@ -1,6 +1,5 @@
|
|||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
import { GetMembership } from "@/modules/auth/decorators/get-membership/get-membership.decorator";
|
|
||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
@@ -36,7 +35,6 @@ import { plainToClass } from "class-transformer";
|
|||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { SkipTakePagination } from "@calcom/platform-types";
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
import { Membership } from "@calcom/prisma/client";
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/organizations/:orgId/memberships",
|
path: "/v2/organizations/:orgId/memberships",
|
||||||
@@ -89,7 +87,11 @@ export class OrganizationsMembershipsController {
|
|||||||
@UseGuards(IsMembershipInOrg)
|
@UseGuards(IsMembershipInOrg)
|
||||||
@Get("/:membershipId")
|
@Get("/:membershipId")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getUserSchedule(@GetMembership() membership: Membership): Promise<GetOrgMembership> {
|
async getOrgMembership(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Param("membershipId", ParseIntPipe) membershipId: number
|
||||||
|
): Promise<GetOrgMembership> {
|
||||||
|
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { IsWebhookInOrg } from "@/modules/auth/guards/organizations/is-webhook-in-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
|
||||||
|
import { CreateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||||
|
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||||
|
import {
|
||||||
|
TeamWebhookOutputDto as OrgWebhookOutputDto,
|
||||||
|
TeamWebhookOutputResponseDto as OrgWebhookOutputResponseDto,
|
||||||
|
TeamWebhooksOutputResponseDto as OrgWebhooksOutputResponseDto,
|
||||||
|
} from "@/modules/webhooks/outputs/team-webhook.output";
|
||||||
|
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||||
|
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||||
|
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
UseGuards,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Query,
|
||||||
|
Delete,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId/webhooks",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@DocsTags("Organizations Webhooks")
|
||||||
|
export class OrganizationsWebhooksController {
|
||||||
|
constructor(
|
||||||
|
private organizationsWebhooksService: OrganizationsWebhooksService,
|
||||||
|
private webhooksService: WebhooksService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getAllOrganizationWebhooks(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Query() queryParams: SkipTakePagination
|
||||||
|
): Promise<OrgWebhooksOutputResponseDto> {
|
||||||
|
const { skip, take } = queryParams;
|
||||||
|
const webhooks = await this.organizationsWebhooksService.getWebhooksPaginated(
|
||||||
|
orgId,
|
||||||
|
skip ?? 0,
|
||||||
|
take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: webhooks.map((webhook) =>
|
||||||
|
plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||||
|
strategy: "excludeAll",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/")
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
async createOrganizationWebhook(
|
||||||
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
|
@Body() body: CreateWebhookInputDto
|
||||||
|
): Promise<OrgWebhookOutputResponseDto> {
|
||||||
|
const webhook = await this.organizationsWebhooksService.createWebhook(
|
||||||
|
orgId,
|
||||||
|
new WebhookInputPipe().transform(body)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||||
|
strategy: "excludeAll",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(IsWebhookInOrg)
|
||||||
|
@Get("/:webhookId")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getOrganizationWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> {
|
||||||
|
const webhook = await this.organizationsWebhooksService.getWebhook(webhookId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||||
|
strategy: "excludeAll",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(IsWebhookInOrg)
|
||||||
|
@Delete("/:webhookId")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async deleteWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> {
|
||||||
|
const webhook = await this.webhooksService.deleteWebhook(webhookId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||||
|
strategy: "excludeAll",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles("ORG_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(IsWebhookInOrg)
|
||||||
|
@Patch("/:webhookId")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async updateOrgWebhook(
|
||||||
|
@Param("webhookId") webhookId: string,
|
||||||
|
@Body() body: UpdateWebhookInputDto
|
||||||
|
): Promise<OrgWebhookOutputResponseDto> {
|
||||||
|
const webhook = await this.organizationsWebhooksService.updateWebhook(
|
||||||
|
webhookId,
|
||||||
|
new PartialWebhookInputPipe().transform(body)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||||
|
strategy: "excludeAll",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||||
|
import {
|
||||||
|
TeamWebhookOutputResponseDto,
|
||||||
|
TeamWebhooksOutputResponseDto,
|
||||||
|
} from "@/modules/webhooks/outputs/team-webhook.output";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { Team, Webhook } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("WebhooksController (e2e)", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
const userEmail = "webhook-controller-e2e@api.com";
|
||||||
|
let org: Team;
|
||||||
|
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let webhookRepositoryFixture: WebhookRepositoryFixture;
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let webhook: TeamWebhookOutputResponseDto["data"];
|
||||||
|
let otherWebhook: Webhook;
|
||||||
|
let user: UserWithProfile;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: "Test Organization",
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
otherWebhook = await webhookRepositoryFixture.create({
|
||||||
|
id: "2mdfnn2",
|
||||||
|
subscriberUrl: "https://example.com",
|
||||||
|
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||||
|
active: true,
|
||||||
|
payloadTemplate: "string",
|
||||||
|
});
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
webhookRepositoryFixture.delete(otherWebhook.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
expect(org).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks (POST)", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/webhooks`)
|
||||||
|
.send({
|
||||||
|
subscriberUrl: "https://example.com",
|
||||||
|
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||||
|
active: true,
|
||||||
|
payloadTemplate: "string",
|
||||||
|
} satisfies CreateWebhookInputDto)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (res) => {
|
||||||
|
process.stdout.write(JSON.stringify(res.body));
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
status: "success",
|
||||||
|
data: {
|
||||||
|
id: expect.any(String),
|
||||||
|
subscriberUrl: "https://example.com",
|
||||||
|
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||||
|
active: true,
|
||||||
|
payloadTemplate: "string",
|
||||||
|
teamId: org.id,
|
||||||
|
},
|
||||||
|
} satisfies TeamWebhookOutputResponseDto);
|
||||||
|
webhook = res.body.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks (POST) should fail to create a webhook that already has same orgId / subcriberUrl combo", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/webhooks`)
|
||||||
|
.send({
|
||||||
|
subscriberUrl: "https://example.com",
|
||||||
|
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||||
|
active: true,
|
||||||
|
payloadTemplate: "string",
|
||||||
|
} satisfies CreateWebhookInputDto)
|
||||||
|
.expect(409);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks/:webhookId (PATCH)", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/v2/organizations/${org.id}/webhooks/${webhook.id}`)
|
||||||
|
.send({
|
||||||
|
active: false,
|
||||||
|
} satisfies UpdateWebhookInputDto)
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
expect(res.body.data.active).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks/:webhookId (GET)", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/webhooks/${webhook.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
status: "success",
|
||||||
|
data: {
|
||||||
|
id: expect.any(String),
|
||||||
|
subscriberUrl: "https://example.com",
|
||||||
|
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||||
|
active: false,
|
||||||
|
payloadTemplate: "string",
|
||||||
|
teamId: org.id,
|
||||||
|
},
|
||||||
|
} satisfies TeamWebhookOutputResponseDto);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks/:webhookId (GET) should say forbidden to get a webhook that does not exist", () => {
|
||||||
|
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/webhooks/90284`).expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to org", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks (GET)", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/webhooks`)
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const responseBody = res.body as TeamWebhooksOutputResponseDto;
|
||||||
|
responseBody.data.forEach((webhook) => {
|
||||||
|
expect(webhook.teamId).toBe(org.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks/:webhookId (DELETE)", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/v2/organizations/${org.id}/webhooks/${webhook.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
status: "success",
|
||||||
|
data: {
|
||||||
|
id: expect.any(String),
|
||||||
|
subscriberUrl: "https://example.com",
|
||||||
|
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||||
|
active: false,
|
||||||
|
payloadTemplate: "string",
|
||||||
|
teamId: org.id,
|
||||||
|
},
|
||||||
|
} satisfies TeamWebhookOutputResponseDto);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||||
|
return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/webhooks/12993`).expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/organizations/:orgId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to org", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class AssignOrganizationAttributeOptionToUserInput {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly value?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly attributeOptionId?: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
readonly attributeId!: string;
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||||
|
import { AttributeType } from "@prisma/client";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
export class CreateOrganizationAttributeInput {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly name!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly slug!: string;
|
||||||
|
|
||||||
|
@IsEnum(AttributeType)
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly type!: AttributeType;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => CreateOrganizationAttributeOptionInput)
|
||||||
|
readonly options!: CreateOrganizationAttributeOptionInput[];
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
readonly enabled?: boolean;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { IsNotEmpty, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateOrganizationAttributeOptionInput {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly value!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly slug!: string;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class UpdateOrganizationAttributeOptionInput {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
readonly value?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
readonly slug?: string;
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { AttributeType } from "@prisma/client";
|
||||||
|
import { IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class UpdateOrganizationAttributeInput {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
readonly name?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
readonly slug?: string;
|
||||||
|
|
||||||
|
@IsEnum(AttributeType)
|
||||||
|
@IsOptional()
|
||||||
|
readonly type?: AttributeType;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
readonly enabled?: boolean;
|
||||||
|
}
|
||||||
@@ -3,19 +3,27 @@ import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/
|
|||||||
import { EmailModule } from "@/modules/email/email.module";
|
import { EmailModule } from "@/modules/email/email.module";
|
||||||
import { EmailService } from "@/modules/email/email.service";
|
import { EmailService } from "@/modules/email/email.service";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
|
import { OrganizationsOptionsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes-options.controller";
|
||||||
|
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
|
||||||
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
|
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
|
||||||
import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller";
|
import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller";
|
||||||
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
|
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
|
||||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
||||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||||
|
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||||
|
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/repositories/attributes/organization-attribute-option.repository";
|
||||||
|
import { OrganizationAttributesRepository } from "@/modules/organizations/repositories/attributes/organization-attribute.repository";
|
||||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
|
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository";
|
import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository";
|
||||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/repositories/organizations-teams-memberships.repository";
|
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/repositories/organizations-teams-memberships.repository";
|
||||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||||
import { OrganizationsUsersRepository } from "@/modules/organizations/repositories/organizations-users.repository";
|
import { OrganizationsUsersRepository } from "@/modules/organizations/repositories/organizations-users.repository";
|
||||||
|
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
|
||||||
|
import { OrganizationAttributeOptionService } from "@/modules/organizations/services/attributes/organization-attributes-option.service";
|
||||||
|
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
|
||||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
|
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
|
||||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||||
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
|
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
|
||||||
@@ -24,11 +32,14 @@ import { OrganizationsSchedulesService } from "@/modules/organizations/services/
|
|||||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service";
|
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service";
|
||||||
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
||||||
import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service";
|
import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service";
|
||||||
|
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
|
||||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { RedisModule } from "@/modules/redis/redis.module";
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||||
|
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -60,6 +71,14 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsEventTypesRepository,
|
OrganizationsEventTypesRepository,
|
||||||
OrganizationsTeamsMembershipsRepository,
|
OrganizationsTeamsMembershipsRepository,
|
||||||
OrganizationsTeamsMembershipsService,
|
OrganizationsTeamsMembershipsService,
|
||||||
|
OrganizationAttributesService,
|
||||||
|
OrganizationAttributeOptionService,
|
||||||
|
OrganizationAttributeOptionRepository,
|
||||||
|
OrganizationAttributesRepository,
|
||||||
|
OrganizationsWebhooksRepository,
|
||||||
|
OrganizationsWebhooksService,
|
||||||
|
WebhooksRepository,
|
||||||
|
WebhooksService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
OrganizationsService,
|
OrganizationsService,
|
||||||
@@ -71,6 +90,14 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsMembershipService,
|
OrganizationsMembershipService,
|
||||||
OrganizationsTeamsMembershipsRepository,
|
OrganizationsTeamsMembershipsRepository,
|
||||||
OrganizationsTeamsMembershipsService,
|
OrganizationsTeamsMembershipsService,
|
||||||
|
OrganizationAttributesService,
|
||||||
|
OrganizationAttributeOptionService,
|
||||||
|
OrganizationAttributeOptionRepository,
|
||||||
|
OrganizationAttributesRepository,
|
||||||
|
OrganizationsWebhooksRepository,
|
||||||
|
OrganizationsWebhooksService,
|
||||||
|
WebhooksRepository,
|
||||||
|
WebhooksService,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
OrganizationsTeamsController,
|
OrganizationsTeamsController,
|
||||||
@@ -79,6 +106,9 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsMembershipsController,
|
OrganizationsMembershipsController,
|
||||||
OrganizationsEventTypesController,
|
OrganizationsEventTypesController,
|
||||||
OrganizationsTeamsMembershipsController,
|
OrganizationsTeamsMembershipsController,
|
||||||
|
OrganizationsAttributesController,
|
||||||
|
OrganizationsOptionsAttributesController,
|
||||||
|
OrganizationsWebhooksController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsString, IsInt, IsEnum, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
export const AttributeType = {
|
||||||
|
TEXT: "TEXT",
|
||||||
|
NUMBER: "NUMBER",
|
||||||
|
SINGLE_SELECT: "SINGLE_SELECT",
|
||||||
|
MULTI_SELECT: "MULTI_SELECT",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AttributeType = (typeof AttributeType)[keyof typeof AttributeType];
|
||||||
|
|
||||||
|
export class Attribute {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The ID of the attribute", example: "attr_123" })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiProperty({
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
description: "The team ID associated with the attribute",
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
|
teamId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
description: "The type of the attribute",
|
||||||
|
enum: AttributeType,
|
||||||
|
})
|
||||||
|
@IsEnum(AttributeType)
|
||||||
|
type!: AttributeType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
description: "The name of the attribute",
|
||||||
|
example: "Attribute Name",
|
||||||
|
})
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
description: "The slug of the attribute",
|
||||||
|
example: "attribute-name",
|
||||||
|
})
|
||||||
|
slug!: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiProperty({
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
description: "Whether the attribute is enabled and displayed on their profile",
|
||||||
|
example: true,
|
||||||
|
})
|
||||||
|
enabled!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiProperty({
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
description: "Whether users can edit the relation",
|
||||||
|
example: true,
|
||||||
|
})
|
||||||
|
usersCanEditRelation!: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class BaseOutputDTO {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateOrganizationAttributesOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Attribute)
|
||||||
|
data!: Attribute;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class DeleteOrganizationAttributesOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Attribute)
|
||||||
|
data!: Attribute;
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsBoolean, IsInt, IsString, IsEnum, ValidateNested, IsOptional } from "class-validator";
|
||||||
|
|
||||||
|
import { AttributeType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
export class GetSingleAttributeOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Attribute)
|
||||||
|
data!: Attribute | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetOrganizationAttributesOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Attribute)
|
||||||
|
data!: Attribute[];
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
class AssignOptionUserOutputData {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The ID of the option assigned to the user" })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: Number, required: true, description: "The ID form the org membership for the user" })
|
||||||
|
memberId!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The value of the option" })
|
||||||
|
attributeOptionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AssignOptionUserOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => AssignOptionUserOutputData)
|
||||||
|
data!: AssignOptionUserOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnassignOptionUserOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => AssignOptionUserOutputData)
|
||||||
|
data!: AssignOptionUserOutputData;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateAttributeOptionOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => OptionOutput)
|
||||||
|
data!: OptionOutput;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class DeleteAttributeOptionOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => OptionOutput)
|
||||||
|
data!: OptionOutput;
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
class GetOptionUserOutputData {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The ID of the option assigned to the user" })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The ID of the attribute" })
|
||||||
|
attributeId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The value of the option" })
|
||||||
|
value!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The slug of the option" })
|
||||||
|
slug!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetOptionUserOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => GetOptionUserOutputData)
|
||||||
|
data!: GetOptionUserOutputData[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class GetAllAttributeOptionOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => OptionOutput)
|
||||||
|
data!: OptionOutput[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsString, IsInt, IsEnum, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
export class OptionOutput {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
description: "The ID of the option",
|
||||||
|
example: "attr_option_id",
|
||||||
|
})
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: String, required: true, description: "The ID of the attribute", example: "attr_id" })
|
||||||
|
attributeId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
description: "The value of the option",
|
||||||
|
example: "option_value",
|
||||||
|
})
|
||||||
|
value!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
description: "The slug of the option",
|
||||||
|
example: "option-slug",
|
||||||
|
})
|
||||||
|
slug!: string;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { OptionOutput } from "@/modules/organizations/outputs/attributes/options/option.output";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class UpdateAttributeOptionOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => OptionOutput)
|
||||||
|
data!: OptionOutput;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||||
|
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class UpdateOrganizationAttributesOutput extends BaseOutputDTO {
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Attribute)
|
||||||
|
data!: Attribute;
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||||
|
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||||
|
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { slugify } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationAttributeOptionRepository {
|
||||||
|
private readonly logger = new Logger("OrganizationAttributeOptionRepository");
|
||||||
|
constructor(
|
||||||
|
private readonly dbRead: PrismaReadService,
|
||||||
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private readonly organizationsMembershipsService: OrganizationsMembershipService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createOrganizationAttributeOption(
|
||||||
|
organizationId: number,
|
||||||
|
attributeId: string,
|
||||||
|
data: CreateOrganizationAttributeOptionInput
|
||||||
|
) {
|
||||||
|
return this.dbWrite.prisma.attributeOption.create({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
attributeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrganizationAttributeOption(organizationId: number, attributeId: string, optionId: string) {
|
||||||
|
try {
|
||||||
|
const deletedAttributeOption = await this.dbWrite.prisma.attributeOption.delete({
|
||||||
|
where: {
|
||||||
|
id: optionId,
|
||||||
|
attributeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return deletedAttributeOption;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "P2025") {
|
||||||
|
// P2025 is the Prisma error code for "Record to delete does not exist."
|
||||||
|
this.logger.warn(`Attribute option not found: ${optionId}`);
|
||||||
|
throw new NotFoundException("Attribute option not found");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrganizationAttributeOption(
|
||||||
|
organizationId: number,
|
||||||
|
attributeId: string,
|
||||||
|
optionId: string,
|
||||||
|
data: UpdateOrganizationAttributeOptionInput
|
||||||
|
) {
|
||||||
|
return this.dbWrite.prisma.attributeOption.update({
|
||||||
|
where: {
|
||||||
|
id: optionId,
|
||||||
|
attributeId,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttributeOptions(organizationId: number, attributeId: string) {
|
||||||
|
return this.dbRead.prisma.attributeOption.findMany({
|
||||||
|
where: {
|
||||||
|
attributeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttributeOptionsForUser(organizationId: number, userId: number) {
|
||||||
|
const options = this.dbRead.prisma.attributeOption.findMany({
|
||||||
|
where: {
|
||||||
|
attribute: {
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
assignedUsers: {
|
||||||
|
some: {
|
||||||
|
member: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignOrganizationAttributeOptionToUser({
|
||||||
|
organizationId,
|
||||||
|
membershipId,
|
||||||
|
attributeId,
|
||||||
|
value,
|
||||||
|
attributeOptionId,
|
||||||
|
}: {
|
||||||
|
organizationId: number;
|
||||||
|
membershipId: number;
|
||||||
|
attributeId: string;
|
||||||
|
value?: string;
|
||||||
|
attributeOptionId?: string;
|
||||||
|
}) {
|
||||||
|
let _attributeOptionId = attributeOptionId;
|
||||||
|
|
||||||
|
if (value && !attributeOptionId) {
|
||||||
|
_attributeOptionId = await this.createDynamicAttributeOption(organizationId, attributeId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_attributeOptionId) throw new Error("Attribute option not found");
|
||||||
|
|
||||||
|
return this.dbWrite.prisma.attributeToUser.create({
|
||||||
|
data: {
|
||||||
|
attributeOptionId: _attributeOptionId,
|
||||||
|
memberId: membershipId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDynamicAttributeOption(
|
||||||
|
organizationId: number,
|
||||||
|
attributeId: string,
|
||||||
|
value: string
|
||||||
|
): Promise<string> {
|
||||||
|
const attributeOption = await this.createOrganizationAttributeOption(organizationId, attributeId, {
|
||||||
|
value: value,
|
||||||
|
slug: slugify(value),
|
||||||
|
});
|
||||||
|
return attributeOption.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unassignOrganizationAttributeOptionFromUser(
|
||||||
|
organizationId: number,
|
||||||
|
userId: number,
|
||||||
|
attributeOptionId: string
|
||||||
|
) {
|
||||||
|
const membership = await this.organizationsMembershipsService.getOrgMembershipByUserId(
|
||||||
|
organizationId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!membership) throw new Error("Membership not found");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deletedAttributeToUser = await this.dbWrite.prisma.attributeToUser.delete({
|
||||||
|
where: { memberId_attributeOptionId: { memberId: membership.id, attributeOptionId } },
|
||||||
|
});
|
||||||
|
return deletedAttributeToUser;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "P2025") {
|
||||||
|
// P2025 is the Prisma error code for "Record to delete does not exist."
|
||||||
|
this.logger.warn(`Attribute option not found: ${attributeOptionId} for user ${userId}`);
|
||||||
|
throw new NotFoundException("Attribute does not belong to this user");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||||
|
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationAttributesRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async createOrganizationAttribute(organizationId: number, data: CreateOrganizationAttributeInput) {
|
||||||
|
const { options, ...attributeData } = data;
|
||||||
|
|
||||||
|
const attribute = await this.dbWrite.prisma.attribute.create({
|
||||||
|
data: {
|
||||||
|
...attributeData,
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attribute.type === "SINGLE_SELECT" || attribute.type === "MULTI_SELECT") {
|
||||||
|
// TODO: move this to attribute option service
|
||||||
|
await this.dbWrite.prisma.attributeOption.createMany({
|
||||||
|
data: options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
attributeId: attribute.id,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttribute(organizationId: number, attributeId: string) {
|
||||||
|
const attribute = await this.dbRead.prisma.attribute.findUnique({
|
||||||
|
where: {
|
||||||
|
id: attributeId,
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttributes(organizationId: number, skip?: number, take?: number) {
|
||||||
|
const attributes = await this.dbRead.prisma.attribute.findMany({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
async updateOrganizationAttribute(
|
||||||
|
organizationId: number,
|
||||||
|
attributeId: string,
|
||||||
|
data: UpdateOrganizationAttributeInput
|
||||||
|
) {
|
||||||
|
const attribute = await this.dbWrite.prisma.attribute.update({
|
||||||
|
where: {
|
||||||
|
id: attributeId,
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrganizationAttribute(organizationId: number, attributeId: string) {
|
||||||
|
const attribute = await this.dbWrite.prisma.attribute.delete({
|
||||||
|
where: {
|
||||||
|
id: attributeId,
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -18,6 +18,15 @@ export class OrganizationsMembershipRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOrgMembershipByUserId(organizationId: number, userId: number) {
|
||||||
|
return this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||||
return this.dbWrite.prisma.membership.delete({
|
return this.dbWrite.prisma.membership.delete({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
+6
-5
@@ -1,11 +1,12 @@
|
|||||||
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
|
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
|
||||||
import { UpdateOrgTeamDto } from "@/modules/organizations/inputs/update-organization-team.input";
|
import { UpdateOrgTeamDto } from "@/modules/organizations/inputs/update-organization-team.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 { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsTeamsRepository {
|
export class OrganizationsTeamsRepository {
|
||||||
constructor(private readonly dbRead: PrismaReadService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async findOrgTeam(organizationId: number, teamId: number) {
|
async findOrgTeam(organizationId: number, teamId: number) {
|
||||||
return this.dbRead.prisma.team.findUnique({
|
return this.dbRead.prisma.team.findUnique({
|
||||||
@@ -26,7 +27,7 @@ export class OrganizationsTeamsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgTeam(organizationId: number, teamId: number) {
|
async deleteOrgTeam(organizationId: number, teamId: number) {
|
||||||
return this.dbRead.prisma.team.delete({
|
return this.dbWrite.prisma.team.delete({
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
id: teamId,
|
||||||
isOrganization: false,
|
isOrganization: false,
|
||||||
@@ -36,13 +37,13 @@ export class OrganizationsTeamsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createOrgTeam(organizationId: number, data: CreateOrgTeamDto) {
|
async createOrgTeam(organizationId: number, data: CreateOrgTeamDto) {
|
||||||
return this.dbRead.prisma.team.create({
|
return this.dbWrite.prisma.team.create({
|
||||||
data: { ...data, parentId: organizationId },
|
data: { ...data, parentId: organizationId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPlatformOrgTeam(organizationId: number, oAuthClientId: string, data: CreateOrgTeamDto) {
|
async createPlatformOrgTeam(organizationId: number, oAuthClientId: string, data: CreateOrgTeamDto) {
|
||||||
return this.dbRead.prisma.team.create({
|
return this.dbWrite.prisma.team.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
parentId: organizationId,
|
parentId: organizationId,
|
||||||
@@ -61,7 +62,7 @@ export class OrganizationsTeamsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateOrgTeam(organizationId: number, teamId: number, data: UpdateOrgTeamDto) {
|
async updateOrgTeam(organizationId: number, teamId: number, data: UpdateOrgTeamDto) {
|
||||||
return this.dbRead.prisma.team.update({
|
return this.dbWrite.prisma.team.update({
|
||||||
data: { ...data },
|
data: { ...data },
|
||||||
where: { id: teamId, parentId: organizationId, isOrganization: false },
|
where: { id: teamId, parentId: organizationId, isOrganization: false },
|
||||||
});
|
});
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { Webhook } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
type WebhookInputData = Pick<
|
||||||
|
Webhook,
|
||||||
|
"payloadTemplate" | "eventTriggers" | "subscriberUrl" | "secret" | "active"
|
||||||
|
>;
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationsWebhooksRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async findWebhookByUrl(organizationId: number, subscriberUrl: string) {
|
||||||
|
return this.dbRead.prisma.webhook.findFirst({
|
||||||
|
where: { teamId: organizationId, subscriberUrl },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWebhook(organizationId: number, webhookId: string) {
|
||||||
|
return this.dbRead.prisma.webhook.findUnique({
|
||||||
|
where: {
|
||||||
|
id: webhookId,
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWebhooks(organizationId: number) {
|
||||||
|
return this.dbRead.prisma.webhook.findMany({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteWebhook(organizationId: number, webhookId: string) {
|
||||||
|
return this.dbRead.prisma.webhook.delete({
|
||||||
|
where: {
|
||||||
|
id: webhookId,
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWebhook(organizationId: number, data: WebhookInputData) {
|
||||||
|
const id = uuidv4();
|
||||||
|
return this.dbWrite.prisma.webhook.create({
|
||||||
|
data: { ...data, id, teamId: organizationId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWebhook(organizationId: number, webhookId: string, data: Partial<WebhookInputData>) {
|
||||||
|
return this.dbRead.prisma.webhook.update({
|
||||||
|
data: { ...data },
|
||||||
|
where: { id: webhookId, teamId: organizationId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWebhooksPaginated(organizationId: number, skip: number, take: number) {
|
||||||
|
return this.dbRead.prisma.webhook.findMany({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/inputs/attributes/assign/organizations-attributes-options-assign.input";
|
||||||
|
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||||
|
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/update-organizaiton-attribute-option.input.ts";
|
||||||
|
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/repositories/attributes/organization-attribute-option.repository";
|
||||||
|
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
|
||||||
|
import { OrganizationsMembershipService } from "@/modules/organizations/services/organizations-membership.service";
|
||||||
|
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
const TYPE_SUPPORTS_VALUE = new Set(["TEXT", "NUMBER"]);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationAttributeOptionService {
|
||||||
|
private readonly logger = new Logger("OrganizationAttributeOptionService");
|
||||||
|
constructor(
|
||||||
|
private readonly organizationAttributeOptionRepository: OrganizationAttributeOptionRepository,
|
||||||
|
private readonly organizationAttributesService: OrganizationAttributesService,
|
||||||
|
private readonly organizationsMembershipsService: OrganizationsMembershipService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createOrganizationAttributeOption(
|
||||||
|
organizationId: number,
|
||||||
|
attributeId: string,
|
||||||
|
data: CreateOrganizationAttributeOptionInput
|
||||||
|
) {
|
||||||
|
return this.organizationAttributeOptionRepository.createOrganizationAttributeOption(
|
||||||
|
organizationId,
|
||||||
|
attributeId,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrganizationAttributeOption(organizationId: number, attributeId: string, optionId: string) {
|
||||||
|
return this.organizationAttributeOptionRepository.deleteOrganizationAttributeOption(
|
||||||
|
organizationId,
|
||||||
|
attributeId,
|
||||||
|
optionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrganizationAttributeOption(
|
||||||
|
organizationId: number,
|
||||||
|
attributeId: string,
|
||||||
|
optionId: string,
|
||||||
|
data: UpdateOrganizationAttributeOptionInput
|
||||||
|
) {
|
||||||
|
return this.organizationAttributeOptionRepository.updateOrganizationAttributeOption(
|
||||||
|
organizationId,
|
||||||
|
attributeId,
|
||||||
|
optionId,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttributeOptions(organizationId: number, attributeId: string) {
|
||||||
|
return this.organizationAttributeOptionRepository.getOrganizationAttributeOptions(
|
||||||
|
organizationId,
|
||||||
|
attributeId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignOrganizationAttributeOptionToUser(
|
||||||
|
organizationId: number,
|
||||||
|
userId: number,
|
||||||
|
data: AssignOrganizationAttributeOptionToUserInput
|
||||||
|
) {
|
||||||
|
const attribute = await this.organizationAttributesService.getOrganizationAttribute(
|
||||||
|
organizationId,
|
||||||
|
data.attributeId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!attribute) {
|
||||||
|
throw new NotFoundException("Attribute not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await this.organizationsMembershipsService.getOrgMembershipByUserId(
|
||||||
|
organizationId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!membership || !membership.accepted)
|
||||||
|
throw new NotFoundException("User is not a member of the organization");
|
||||||
|
|
||||||
|
if (!TYPE_SUPPORTS_VALUE.has(attribute.type) && data.value) {
|
||||||
|
throw new BadRequestException("Attribute type does not support value");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.organizationAttributeOptionRepository.assignOrganizationAttributeOptionToUser({
|
||||||
|
organizationId,
|
||||||
|
membershipId: membership.id,
|
||||||
|
value: data.value,
|
||||||
|
attributeId: data.attributeId,
|
||||||
|
attributeOptionId: data.attributeOptionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unassignOrganizationAttributeOptionFromUser(
|
||||||
|
organizationId: number,
|
||||||
|
userId: number,
|
||||||
|
attributeOptionId: string
|
||||||
|
) {
|
||||||
|
return this.organizationAttributeOptionRepository.unassignOrganizationAttributeOptionFromUser(
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
attributeOptionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttributeOptionsForUser(organizationId: number, userId: number) {
|
||||||
|
return this.organizationAttributeOptionRepository.getOrganizationAttributeOptionsForUser(
|
||||||
|
organizationId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user