Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
+24
-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
|
||||||
@@ -348,6 +350,19 @@ 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_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 +377,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
|
||||||
@@ -14,7 +14,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Label PR as ready for E2E
|
- name: Label PR as ready for E2E
|
||||||
if: github.event.review.state == 'approved'
|
if: github.event.review.state == 'approved'
|
||||||
uses: actions-ecosystem/action-add-labels@v1
|
uses: actions/github-script@v4
|
||||||
with:
|
with:
|
||||||
|
script: |
|
||||||
|
await github.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
labels: process.env.labels.split(', '),
|
||||||
|
})
|
||||||
|
env:
|
||||||
github_token: ${{ secrets.READY_FOR_E2E_PAT }}
|
github_token: ${{ secrets.READY_FOR_E2E_PAT }}
|
||||||
labels: 'ready-for-e2e'
|
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
|
||||||
|
|||||||
@@ -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,8 +18,15 @@ 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 {
|
||||||
|
if (isColdStart && IS_PRODUCTION) {
|
||||||
|
console.log("Cold start of /v1/slots detected");
|
||||||
|
isColdStart = false;
|
||||||
|
}
|
||||||
|
|
||||||
const { usernameList, ...rest } = req.query;
|
const { usernameList, ...rest } = req.query;
|
||||||
let slugs = usernameList;
|
let slugs = usernameList;
|
||||||
if (!Array.isArray(usernameList)) {
|
if (!Array.isArray(usernameList)) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.33",
|
||||||
"@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;
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
+7
-5
@@ -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";
|
||||||
|
|
||||||
@@ -24,7 +25,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 +55,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 +82,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)
|
||||||
@@ -153,7 +155,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 };
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -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())
|
||||||
@@ -253,6 +258,7 @@ 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);
|
||||||
eventType = responseBody.data;
|
eventType = responseBody.data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -278,6 +284,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 +314,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 +323,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 +364,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 +395,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 +421,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 () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { 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";
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ type InputEventTransformed = Omit<
|
|||||||
| "periodCountCalendarDays"
|
| "periodCountCalendarDays"
|
||||||
| "periodStartDate"
|
| "periodStartDate"
|
||||||
| "periodEndDate"
|
| "periodEndDate"
|
||||||
|
| "recurrence"
|
||||||
> & {
|
> & {
|
||||||
length: number;
|
length: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -34,7 +36,11 @@ type InputEventTransformed = Omit<
|
|||||||
|
|
||||||
@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,
|
||||||
|
|||||||
+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 };
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -5,6 +5,7 @@ import {
|
|||||||
transformApiEventTypeLocations,
|
transformApiEventTypeLocations,
|
||||||
transformApiEventTypeIntervalLimits,
|
transformApiEventTypeIntervalLimits,
|
||||||
transformApiEventTypeFutureBookingLimits,
|
transformApiEventTypeFutureBookingLimits,
|
||||||
|
transformApiEventTypeRecurrence,
|
||||||
} from "@calcom/platform-libraries";
|
} 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";
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
bookingLimitsCount,
|
bookingLimitsCount,
|
||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
|
recurrence,
|
||||||
...rest
|
...rest
|
||||||
} = inputEventType;
|
} = inputEventType;
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
? 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,6 +55,7 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
bookingLimitsCount,
|
bookingLimitsCount,
|
||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
|
recurrence,
|
||||||
...rest
|
...rest
|
||||||
} = inputEventType;
|
} = inputEventType;
|
||||||
|
|
||||||
@@ -66,6 +70,7 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||||
: undefined,
|
: undefined,
|
||||||
...this.transformInputBookingWindow(bookingWindow),
|
...this.transformInputBookingWindow(bookingWindow),
|
||||||
|
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return eventType;
|
return eventType;
|
||||||
@@ -87,4 +92,8 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
|
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
|
||||||
return !!res ? res : {};
|
return !!res ? res : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
|
||||||
|
return transformApiEventTypeRecurrence(recurrence);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+5
-3
@@ -14,6 +14,7 @@ import {
|
|||||||
parseBookingLimit,
|
parseBookingLimit,
|
||||||
getResponseEventTypeIntervalLimits,
|
getResponseEventTypeIntervalLimits,
|
||||||
getResponseEventTypeFutureBookingLimits,
|
getResponseEventTypeFutureBookingLimits,
|
||||||
|
getResponseEventTypeRecurrence,
|
||||||
} 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,
|
||||||
@@ -154,7 +155,8 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
|
|
||||||
transformRecurringEvent(recurringEvent: any) {
|
transformRecurringEvent(recurringEvent: any) {
|
||||||
if (!recurringEvent) return null;
|
if (!recurringEvent) return null;
|
||||||
return parseRecurringEvent(recurringEvent);
|
const recurringEventParsed = parseRecurringEvent(recurringEvent);
|
||||||
|
return getResponseEventTypeRecurrence(recurringEventParsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformMetadata(metadata: any) {
|
transformMetadata(metadata: any) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||||
|
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||||
|
import { OrganizationAttributesRepository } from "@/modules/organizations/repositories/attributes/organization-attribute.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationAttributesService {
|
||||||
|
constructor(private readonly organizationAttributesRepository: OrganizationAttributesRepository) {}
|
||||||
|
|
||||||
|
async createOrganizationAttribute(organizationId: number, data: CreateOrganizationAttributeInput) {
|
||||||
|
const attribute = await this.organizationAttributesRepository.createOrganizationAttribute(organizationId, data);
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttribute(organizationId: number, attributeId: string) {
|
||||||
|
const attribute = await this.organizationAttributesRepository.getOrganizationAttribute(organizationId, attributeId);
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationAttributes(organizationId: number, skip?: number, take?: number) {
|
||||||
|
const attributes = await this.organizationAttributesRepository.getOrganizationAttributes(organizationId, skip, take);
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrganizationAttribute(organizationId: number, attributeId: string, data: UpdateOrganizationAttributeInput) {
|
||||||
|
const attribute = await this.organizationAttributesRepository.updateOrganizationAttribute(organizationId, attributeId, data);
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrganizationAttribute(organizationId: number, attributeId: string) {
|
||||||
|
const attribute = await this.organizationAttributesRepository.deleteOrganizationAttribute(organizationId, attributeId);
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
CreateTeamEventTypeInput_2024_06_14,
|
CreateTeamEventTypeInput_2024_06_14,
|
||||||
UpdateTeamEventTypeInput_2024_06_14,
|
UpdateTeamEventTypeInput_2024_06_14,
|
||||||
HostPriority,
|
HostPriority,
|
||||||
SchedulingType,
|
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InputOrganizationsEventTypesService {
|
export class InputOrganizationsEventTypesService {
|
||||||
@@ -30,6 +30,7 @@ export class InputOrganizationsEventTypesService {
|
|||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
bookingFields,
|
bookingFields,
|
||||||
|
recurrence,
|
||||||
...rest
|
...rest
|
||||||
} = inputEventType;
|
} = inputEventType;
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ export class InputOrganizationsEventTypesService {
|
|||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
bookingFields,
|
bookingFields,
|
||||||
|
recurrence,
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -47,6 +47,7 @@ export class OrganizationsEventTypesService {
|
|||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
bookingFields,
|
bookingFields,
|
||||||
|
recurrence,
|
||||||
...rest
|
...rest
|
||||||
} = await this.inputService.transformInputCreateTeamEventType(teamId, body);
|
} = await this.inputService.transformInputCreateTeamEventType(teamId, body);
|
||||||
const { eventType: eventTypeCreated } = await createEventType({
|
const { eventType: eventTypeCreated } = await createEventType({
|
||||||
@@ -69,6 +70,7 @@ export class OrganizationsEventTypesService {
|
|||||||
bookingLimitsDuration,
|
bookingLimitsDuration,
|
||||||
bookingWindow,
|
bookingWindow,
|
||||||
bookingFields,
|
bookingFields,
|
||||||
|
recurrence,
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
user
|
user
|
||||||
@@ -96,7 +98,8 @@ export class OrganizationsEventTypesService {
|
|||||||
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
||||||
const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId);
|
const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId);
|
||||||
const profileId =
|
const profileId =
|
||||||
this.usersService.getUserProfileByOrgId(user, organizationId)?.id || user.movedToProfileId;
|
this.usersService.getUserProfileByOrgId(user, organizationId)?.id ||
|
||||||
|
this.usersService.getUserMainProfile(user)?.id;
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export class OrganizationsMembershipService {
|
|||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrgMembershipByUserId(organizationId: number, userId: number) {
|
||||||
|
const membership = await this.organizationsMembershipRepository.findOrgMembershipByUserId(
|
||||||
|
organizationId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
|
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
|
||||||
const memberships = await this.organizationsMembershipRepository.findOrgMembershipsPaginated(
|
const memberships = await this.organizationsMembershipRepository.findOrgMembershipsPaginated(
|
||||||
organizationId,
|
organizationId,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
|
||||||
|
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||||
|
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||||
|
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||||
|
import { ConflictException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationsWebhooksService {
|
||||||
|
constructor(
|
||||||
|
private readonly organizationsWebhooksRepository: OrganizationsWebhooksRepository,
|
||||||
|
private readonly webhooksRepository: WebhooksRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createWebhook(orgId: number, body: PipedInputWebhookType) {
|
||||||
|
const existingWebhook = await this.organizationsWebhooksRepository.findWebhookByUrl(
|
||||||
|
orgId,
|
||||||
|
body.subscriberUrl
|
||||||
|
);
|
||||||
|
if (existingWebhook) {
|
||||||
|
throw new ConflictException("Webhook with this subscriber url already exists for this user");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.organizationsWebhooksRepository.createWebhook(orgId, {
|
||||||
|
...body,
|
||||||
|
payloadTemplate: body.payloadTemplate ?? null,
|
||||||
|
secret: body.secret ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWebhooksPaginated(orgId: number, skip: number, take: number) {
|
||||||
|
return this.organizationsWebhooksRepository.findWebhooksPaginated(orgId, skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWebhook(webhookId: string) {
|
||||||
|
const webhook = await this.webhooksRepository.getWebhookById(webhookId);
|
||||||
|
if (!webhook) {
|
||||||
|
throw new NotFoundException(`Webhook (${webhookId}) not found`);
|
||||||
|
}
|
||||||
|
return webhook;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWebhook(webhookId: string, body: UpdateWebhookInputDto) {
|
||||||
|
return this.webhooksRepository.updateWebhook(webhookId, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,10 @@ export class UsersService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserMainOrgId(user: UserWithProfile) {
|
||||||
|
return this.getUserMainProfile(user)?.organizationId ?? user.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
getUserProfileByOrgId(user: UserWithProfile, organizationId: number) {
|
getUserProfileByOrgId(user: UserWithProfile, organizationId: number) {
|
||||||
return user.profiles?.find((p) => p.organizationId === organizationId);
|
return user.profiles?.find((p) => p.organizationId === organizationId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||||
import {
|
import {
|
||||||
CanActivate,
|
CanActivate,
|
||||||
@@ -17,7 +18,8 @@ import { PlatformOAuthClient, Webhook } from "@calcom/prisma/client";
|
|||||||
export class IsOAuthClientWebhookGuard implements CanActivate {
|
export class IsOAuthClientWebhookGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly webhooksService: WebhooksService,
|
private readonly webhooksService: WebhooksService,
|
||||||
private readonly oAuthClientRepository: OAuthClientRepository
|
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
@@ -27,7 +29,7 @@ export class IsOAuthClientWebhookGuard implements CanActivate {
|
|||||||
const user = request.user as GetUserReturnType;
|
const user = request.user as GetUserReturnType;
|
||||||
const webhookId = request.params.webhookId;
|
const webhookId = request.params.webhookId;
|
||||||
const oAuthClientId = request.params.clientId;
|
const oAuthClientId = request.params.clientId;
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ForbiddenException("User not authenticated");
|
throw new ForbiddenException("User not authenticated");
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsInt, IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
import { WebhookOutputDto } from "./webhook.output";
|
||||||
|
|
||||||
|
export class TeamWebhookOutputDto extends WebhookOutputDto {
|
||||||
|
@IsInt()
|
||||||
|
@Expose()
|
||||||
|
readonly teamId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamWebhookOutputResponseDto {
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => WebhookOutputDto)
|
||||||
|
data!: TeamWebhookOutputDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamWebhooksOutputResponseDto {
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => WebhookOutputDto)
|
||||||
|
data!: TeamWebhookOutputDto[];
|
||||||
|
}
|
||||||
+1604
-134
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,30 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { TestingModule } from "@nestjs/testing";
|
||||||
|
import { Prisma, User } from "@prisma/client";
|
||||||
|
|
||||||
|
export class AttributeRepositoryFixture {
|
||||||
|
private prismaReadClient: PrismaReadService["prisma"];
|
||||||
|
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||||
|
|
||||||
|
constructor(private readonly module: TestingModule) {
|
||||||
|
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||||
|
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Prisma.AttributeCreateInput) {
|
||||||
|
return this.prismaWriteClient.attribute.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOption(data: Prisma.AttributeOptionCreateInput) {
|
||||||
|
return this.prismaWriteClient.attributeOption.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
return this.prismaWriteClient.attribute.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOption(id: string) {
|
||||||
|
return this.prismaWriteClient.attributeOption.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
"@prisma/client/*": ["@calcom/prisma/client/*"],
|
"@prisma/client/*": ["@calcom/prisma/client/*"],
|
||||||
"@calcom/platform-constants": ["../../../packages/platform/constants/index.ts"],
|
"@calcom/platform-constants": ["../../../packages/platform/constants/index.ts"],
|
||||||
"@calcom/platform-types": ["../../../packages/platform/types/index.ts"],
|
"@calcom/platform-types": ["../../../packages/platform/types/index.ts"],
|
||||||
"@calcom/platform-utils": ["../../../packages/platform/utils/index.ts"]
|
"@calcom/platform-utils": ["../../../packages/platform/utils/index.ts"],
|
||||||
|
"@calcom/platform-enums": ["../../../packages/platform/enums/index.ts"]
|
||||||
},
|
},
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
@@ -8,11 +8,21 @@ import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME }
|
|||||||
const ROUTES: [URLPattern, boolean][] = [
|
const ROUTES: [URLPattern, boolean][] = [
|
||||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||||
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||||
|
["/settings/platform/:path*", process.env.APP_ROUTER_SETTINGS_PLATFORM_ENABLED === "1"] as const,
|
||||||
|
["/settings/organizations/:path*", process.env.APP_ROUTER_SETTINGS_ORG_ENABLED === "1"] as const,
|
||||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||||
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
||||||
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
||||||
|
["/auth/forgot-password/:path*", process.env.APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED === "1"] as const,
|
||||||
|
["/auth/login", process.env.APP_ROUTER_AUTH_LOGIN_ENABLED === "1"] as const,
|
||||||
|
["/auth/logout", process.env.APP_ROUTER_AUTH_LOGOUT_ENABLED === "1"] as const,
|
||||||
|
["/auth/new", process.env.APP_ROUTER_AUTH_NEW_ENABLED === "1"] as const,
|
||||||
|
["/auth/saml-idp", process.env.APP_ROUTER_AUTH_SAML_ENABLED === "1"] as const,
|
||||||
|
["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const,
|
||||||
|
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
|
||||||
|
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
|
||||||
["/workflows/:path*", process.env.APP_ROUTER_WORKFLOWS_ENABLED === "1"] as const,
|
["/workflows/:path*", process.env.APP_ROUTER_WORKFLOWS_ENABLED === "1"] as const,
|
||||||
["/settings/teams/:path*", process.env.APP_ROUTER_SETTINGS_TEAMS_ENABLED === "1"] as const,
|
["/settings/teams/:path*", process.env.APP_ROUTER_SETTINGS_TEAMS_ENABLED === "1"] as const,
|
||||||
["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const,
|
["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const,
|
||||||
@@ -20,6 +30,11 @@ const ROUTES: [URLPattern, boolean][] = [
|
|||||||
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
|
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
|
||||||
["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const,
|
["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const,
|
||||||
["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const,
|
["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const,
|
||||||
|
["/more", process.env.APP_ROUTER_MORE_ENABLED === "1"] as const,
|
||||||
|
["/maintenance", process.env.APP_ROUTER_MAINTENANCE_ENABLED === "1"] as const,
|
||||||
|
["/upgrade", process.env.APP_ROUTER_UPGRADE_ENABLED === "1"] as const,
|
||||||
|
["/connect-and-join", process.env.APP_ROUTER_CONNECT_AND_JOIN_ENABLED === "1"] as const,
|
||||||
|
["/availability", process.env.APP_ROUTER_AVAILABILITY_ENABLED === "1"] as const,
|
||||||
].map(([pathname, enabled]) => [
|
].map(([pathname, enabled]) => [
|
||||||
new URLPattern({
|
new URLPattern({
|
||||||
pathname,
|
pathname,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const create = async (locale: string, ns: string) => {
|
|||||||
return _i18n;
|
return _i18n;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFixedT = async (locale: string, ns: string) => {
|
export const getFixedT = async (locale: string, ns = "common") => {
|
||||||
const i18n = await create(locale, ns);
|
const i18n = await create(locale, ns);
|
||||||
|
|
||||||
return i18n.getFixedT(locale, ns);
|
return i18n.getFixedT(locale, ns);
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||||
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { WithLayout } from "app/layoutHOC";
|
import { WithLayout } from "app/layoutHOC";
|
||||||
import { type GetServerSidePropsContext } from "next";
|
|
||||||
import { headers, cookies } from "next/headers";
|
import { headers, cookies } from "next/headers";
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import LegacyPage from "~/users/views/users-type-public-view";
|
import LegacyPage from "~/users/views/users-type-public-view";
|
||||||
import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
import {
|
||||||
|
getServerSideProps,
|
||||||
|
type PageProps as LegacyPageProps,
|
||||||
|
} from "~/users/views/users-type-public-view.getServerSideProps";
|
||||||
|
|
||||||
export const generateMetadata = async ({
|
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||||
params,
|
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
params: Record<string, string | string[]>;
|
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
|
||||||
}) => {
|
|
||||||
const props = await getData(
|
|
||||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
|
||||||
);
|
|
||||||
|
|
||||||
const { eventData, booking, user, slug } = props;
|
const { eventData, booking, user, slug } = props;
|
||||||
const rescheduleUid = booking?.uid;
|
const rescheduleUid = booking?.uid;
|
||||||
@@ -42,7 +37,7 @@ export const generateMetadata = async ({
|
|||||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||||
|
|
||||||
export default WithLayout({
|
export default WithLayout({
|
||||||
getData,
|
getData,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||||
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { WithLayout } from "app/layoutHOC";
|
import { WithLayout } from "app/layoutHOC";
|
||||||
import { type GetServerSidePropsContext } from "next";
|
|
||||||
import { headers, cookies } from "next/headers";
|
import { headers, cookies } from "next/headers";
|
||||||
|
|
||||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||||
@@ -11,16 +11,8 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|||||||
import LegacyPage from "~/users/views/users-public-view";
|
import LegacyPage from "~/users/views/users-public-view";
|
||||||
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||||
|
|
||||||
export const generateMetadata = async ({
|
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||||
params,
|
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
params: Record<string, string | string[]>;
|
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
|
||||||
}) => {
|
|
||||||
const props = await getData(
|
|
||||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
|
||||||
);
|
|
||||||
|
|
||||||
const { profile, markdownStrippedBio } = props;
|
const { profile, markdownStrippedBio } = props;
|
||||||
return await _generateMetadata(
|
return await _generateMetadata(
|
||||||
|
|||||||
@@ -1,47 +1,17 @@
|
|||||||
import LegacyPage, { getLayout } from "@pages/apps/[slug]/[...pages]";
|
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||||
import type { PageProps, SearchParams } from "app/_types";
|
import type { SearchParams } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
import type { GetServerSidePropsResult } from "next";
|
||||||
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
import { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps";
|
||||||
import RoutingFormsRoutingConfig, {
|
|
||||||
serverSidePropsConfig,
|
|
||||||
} from "@calcom/app-store/routing-forms/pages/app-routing.config";
|
|
||||||
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
|
|
||||||
|
|
||||||
import type { AppProps } from "@lib/app-providers";
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapperAppDir";
|
import LegacyPage, { getLayout } from "~/apps/[slug]/[...pages]/pages-view";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
type AppPageType = {
|
|
||||||
getServerSideProps: AppGetServerSideProps;
|
|
||||||
// A component than can accept any properties
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
default: ((props: any) => JSX.Element) &
|
|
||||||
Pick<AppProps["Component"], "isBookingPage" | "getLayout" | "PageWrapper">;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Found = {
|
|
||||||
notFound: false;
|
|
||||||
Component: AppPageType["default"];
|
|
||||||
getServerSideProps: AppPageType["getServerSideProps"];
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppsRouting = {
|
|
||||||
"routing-forms": RoutingFormsRoutingConfig,
|
|
||||||
typeform: TypeformRoutingConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
const paramsSchema = z.object({
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
@@ -61,137 +31,22 @@ export const generateMetadata = async ({
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainPage = p.data.pages[0];
|
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||||
|
const data = await getData(legacyContext);
|
||||||
if (mainPage === "forms") {
|
const form = "form" in data ? (data.form as { name?: string; description?: string }) : null;
|
||||||
return await _generateMetadata(
|
const formName = form?.name ?? "Forms";
|
||||||
() => `Forms | ${APP_NAME}`,
|
const formDescription = form?.description ?? "";
|
||||||
() => ""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacyContext = buildLegacyCtx(
|
|
||||||
headers(),
|
|
||||||
cookies(),
|
|
||||||
params,
|
|
||||||
searchParams
|
|
||||||
) as unknown as GetServerSidePropsContext;
|
|
||||||
const { form } = await getPageProps(legacyContext);
|
|
||||||
|
|
||||||
return await _generateMetadata(
|
return await _generateMetadata(
|
||||||
() => `${form.name} | ${APP_NAME}`,
|
() => formName,
|
||||||
() => form.description
|
() => formDescription
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getRoute(appName: string, pages: string[]) {
|
const getData = withAppDirSsr<GetServerSidePropsResult<any>>(getServerSideProps);
|
||||||
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record<string, AppPageType>;
|
|
||||||
|
|
||||||
if (!routingConfig) {
|
export default WithLayout({
|
||||||
notFound();
|
getLayout,
|
||||||
}
|
getData,
|
||||||
|
Page: LegacyPage,
|
||||||
const mainPage = pages[0];
|
});
|
||||||
const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType);
|
|
||||||
|
|
||||||
const getServerSidePropsHandler = serverSidePropsConfig[mainPage];
|
|
||||||
|
|
||||||
if (!appPage) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
notFound: false,
|
|
||||||
Component: appPage.default,
|
|
||||||
...appPage,
|
|
||||||
getServerSideProps: getServerSidePropsHandler,
|
|
||||||
} as Found;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPageProps = async ({ params, query, req }: GetServerSidePropsContext) => {
|
|
||||||
const p = paramsSchema.safeParse(params);
|
|
||||||
|
|
||||||
if (!p.success) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { slug: appName, pages } = p.data;
|
|
||||||
|
|
||||||
const route = getRoute(appName, pages);
|
|
||||||
|
|
||||||
if (route.notFound) {
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.getServerSideProps) {
|
|
||||||
// TODO: Document somewhere that right now it is just a convention that filename should have appPages in it's name.
|
|
||||||
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
|
|
||||||
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
|
|
||||||
params!.appPages = pages.slice(1);
|
|
||||||
|
|
||||||
const ctx = { req, params, query };
|
|
||||||
|
|
||||||
const session = await getServerSession({ req });
|
|
||||||
const user = session?.user;
|
|
||||||
const app = await getAppWithMetadata({ slug: appName });
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await route.getServerSideProps(
|
|
||||||
{
|
|
||||||
...ctx,
|
|
||||||
params: {
|
|
||||||
...ctx.params,
|
|
||||||
appPages: pages.slice(1),
|
|
||||||
},
|
|
||||||
} as GetServerSidePropsContext<{
|
|
||||||
slug: string;
|
|
||||||
pages: string[];
|
|
||||||
appPages: string[];
|
|
||||||
}>,
|
|
||||||
prisma,
|
|
||||||
user,
|
|
||||||
ssrInit
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-ignore
|
|
||||||
|
|
||||||
if (result.notFound) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.redirect) {
|
|
||||||
redirect(result.redirect.destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
appName,
|
|
||||||
appUrl: app.simplePath || `/apps/${appName}`,
|
|
||||||
...result.props,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
appName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function Page({ params, searchParams }: PageProps) {
|
|
||||||
const h = headers();
|
|
||||||
const nonce = h.get("x-nonce") ?? undefined;
|
|
||||||
|
|
||||||
const legacyContext = buildLegacyCtx(
|
|
||||||
h,
|
|
||||||
cookies(),
|
|
||||||
params,
|
|
||||||
searchParams
|
|
||||||
) as unknown as GetServerSidePropsContext;
|
|
||||||
const props = await getPageProps(legacyContext);
|
|
||||||
return (
|
|
||||||
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
|
|
||||||
<LegacyPage {...props} />
|
|
||||||
</PageWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loaded 100 of 929 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user