Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b719ac070 | ||
|
|
baa9045af7 | ||
|
|
b004587262 | ||
|
|
8a7beff339 | ||
|
|
9001eb7795 | ||
|
|
50c4578c2e | ||
|
|
4ff2ce90f9 | ||
|
|
86f8c6c5fa | ||
|
|
0611bbd50e | ||
|
|
b946abab02 | ||
|
|
a091c60618 | ||
|
|
bba7fc1a30 | ||
|
|
864c46736b | ||
|
|
4416709ff6 | ||
|
|
591d42fc8f | ||
|
|
d73b84157a | ||
|
|
680e748b7a | ||
|
|
99b7aaa62d | ||
|
|
3a01e6390f | ||
|
|
a6c25ab7d3 | ||
|
|
df98274a8f | ||
|
|
b540a2ec45 | ||
|
|
8e4459f2fe | ||
|
|
bc5a9f231f | ||
|
|
88984f6175 | ||
|
|
fc592a17ac | ||
|
|
a51524e673 | ||
|
|
cb6553c731 | ||
|
|
101fea2c1c | ||
|
|
aab9193e7b | ||
|
|
a6d2e46b3d | ||
|
|
25ae1493c3 | ||
|
|
56dcab2f6d | ||
|
|
8fbc76d59a | ||
|
|
894c0fc8a7 | ||
|
|
076550b384 | ||
|
|
08e1b0a9c8 | ||
|
|
ef4deb1657 | ||
|
|
f81074c4f5 | ||
|
|
5df14c344e | ||
|
|
cd311f076c | ||
|
|
d60f165223 | ||
|
|
8477128605 | ||
|
|
84606543ae | ||
|
|
a11242e93d | ||
|
|
5939a99aca | ||
|
|
050abd9fab | ||
|
|
1adf2ad5a8 | ||
|
|
3f2d83d89f | ||
|
|
528a4fbb97 | ||
|
|
95e2ad3007 | ||
|
|
c41e5ddc2c | ||
|
|
f206b3d135 | ||
|
|
0b03bcb90e | ||
|
|
b09b32b121 | ||
|
|
d5f7065fdb | ||
|
|
dc40cc664d | ||
|
|
3eaf84e961 | ||
|
|
c36249b5a7 | ||
|
|
3666c260ee | ||
|
|
77f3fd3b38 | ||
|
|
848bcbfb8f | ||
|
|
24936d8b7b | ||
|
|
ad62f9d2bd | ||
|
|
829dd937d6 | ||
|
|
dda4b17a7c | ||
|
|
4def31e98f | ||
|
|
6d3f5495cb | ||
|
|
008af08148 | ||
|
|
ac0930a11f | ||
|
|
0e62800af6 | ||
|
|
b6ab1ef80a | ||
|
|
13347edced | ||
|
|
d734841184 | ||
|
|
a2998b6c84 | ||
|
|
da5df7164a | ||
|
|
3e28160fa7 | ||
|
|
2b9bdb9c80 |
@@ -12,7 +12,7 @@
|
||||
## Mandatory Tasks (DO NOT REMOVE)
|
||||
|
||||
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
|
||||
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com). If N/A, write N/A here and check the checkbox.
|
||||
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com/docs). If N/A, write N/A here and check the checkbox.
|
||||
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
|
||||
|
||||
## How should this be tested?
|
||||
|
||||
@@ -37,8 +37,6 @@ runs:
|
||||
with:
|
||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-download-cache-
|
||||
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore node_modules
|
||||
|
||||
@@ -3,6 +3,10 @@ description: "Install playwright, cache and restore if necessary"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Get installed Playwright version
|
||||
shell: bash
|
||||
id: playwright-version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
|
||||
- name: Cache playwright binaries
|
||||
id: playwright-cache
|
||||
uses: buildjet/cache@v4
|
||||
@@ -11,9 +15,7 @@ runs:
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: cache-playwright-
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
- name: Yarn playwright install
|
||||
shell: bash
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Delete
|
||||
|
||||
on:
|
||||
delete:
|
||||
branches-ignore: [main, gh-pages]
|
||||
|
||||
# ensures that currently running Playwright workflow of deleted branch gets cancelled
|
||||
concurrency:
|
||||
group: ${{ github.event.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
delete_reports:
|
||||
name: Delete Reports
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Contains all reports for deleted branch
|
||||
BRANCH_REPORTS_DIR: reports/${{ github.event.ref }}
|
||||
steps:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: gh-pages
|
||||
- name: Set Git User
|
||||
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: Check for workflow reports
|
||||
run: |
|
||||
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
|
||||
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Delete reports from repo for branch
|
||||
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
cd $BRANCH_REPORTS_DIR/..
|
||||
|
||||
rm -rf ${{ github.event.ref }}
|
||||
git add .
|
||||
git commit -m "workflow: remove all reports for branch ${{ github.event.ref }}"
|
||||
|
||||
while true; do
|
||||
git pull --rebase
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to rebase. Please review manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git push
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Successfully pushed HTML reports to repo."
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
@@ -98,5 +98,5 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-store-results
|
||||
path: test-results
|
||||
name: blob-report-app-store
|
||||
path: blob-report
|
||||
|
||||
@@ -85,5 +85,5 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: embed-react-results
|
||||
path: test-results
|
||||
name: blob-report-embed-react
|
||||
path: blob-report
|
||||
|
||||
@@ -91,5 +91,5 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: embed-core-results
|
||||
path: test-results
|
||||
name: blob-report-embed-core
|
||||
path: blob-report
|
||||
|
||||
@@ -89,8 +89,9 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
name: blob-report-${{ matrix.shard }}
|
||||
path: blob-report
|
||||
retention-days: 30
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-shards
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
merge-reports:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
- name: Merge into HTML Report
|
||||
run: yarn playwright merge-reports --reporter html ./all-blob-reports
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
@@ -7,11 +7,14 @@ on:
|
||||
jobs:
|
||||
label-pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Label PR as ready for E2E
|
||||
if: github.event.review.state == 'approved'
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_token: ${{ secrets.READY_FOR_E2E_PAT }}
|
||||
labels: 'ready-for-e2e'
|
||||
|
||||
@@ -91,44 +91,50 @@ jobs:
|
||||
console.log('Found the label?', labelFound);
|
||||
core.setOutput('run-e2e', labelFound);
|
||||
|
||||
deps:
|
||||
name: Install dependencies
|
||||
needs: [changes, check-label]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/yarn-install.yml
|
||||
|
||||
type-check:
|
||||
name: Type check
|
||||
needs: [changes, check-label]
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
needs: [changes, check-label]
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
unit-test:
|
||||
name: Tests
|
||||
needs: [changes, check-label]
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v1:
|
||||
name: Production builds
|
||||
needs: [changes, check-label]
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v2:
|
||||
name: Production builds
|
||||
needs: [changes, check-label]
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v2-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: [changes, check-label]
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
@@ -181,8 +187,42 @@ jobs:
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
merge-reports:
|
||||
name: Merge reports
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
uses: ./.github/workflows/merge-reports.yml
|
||||
secrets: inherit
|
||||
|
||||
publish-report:
|
||||
name: Publish HTML report
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
needs: [changes, check-label, merge-reports]
|
||||
uses: ./.github/workflows/publish-report.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [changes, lint, type-check, unit-test, integration-test, check-label, build, build-api-v1, build-api-v2, e2e, e2e-api-v2, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs:
|
||||
[
|
||||
changes,
|
||||
lint,
|
||||
type-check,
|
||||
unit-test,
|
||||
integration-test,
|
||||
check-label,
|
||||
build,
|
||||
build-api-v1,
|
||||
build-api-v2,
|
||||
e2e,
|
||||
e2e-api-v2,
|
||||
e2e-embed,
|
||||
e2e-embed-react,
|
||||
e2e-app-store,
|
||||
]
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
on:
|
||||
workflow_call:
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
publish-report:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
env:
|
||||
# Unique URL path for each workflow run attempt
|
||||
HTML_REPORT_URL_PATH: reports/${{ github.head_ref }}/${{ github.run_id }}/${{ github.run_attempt }}
|
||||
BRANCH_REPORTS_DIR: reports/${{ github.head_ref }}
|
||||
steps:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: gh-pages
|
||||
- name: Set Git User
|
||||
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: Check for workflow reports
|
||||
run: |
|
||||
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
|
||||
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Cleanup old HTML reports
|
||||
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
rm -rf $BRANCH_REPORTS_DIR
|
||||
git add .
|
||||
git commit -m "workflow: remove all reports for branch ${{ github.head_ref }}"
|
||||
- name: Download zipped HTML report
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: ${{ env.HTML_REPORT_URL_PATH }}
|
||||
- name: Push HTML Report
|
||||
timeout-minutes: 3
|
||||
# commit report, then try push-rebase-loop until it's able to merge the HTML report to the gh-pages branch
|
||||
# this is necessary when this job running at least twice at the same time (e.g. through two pushes at the same time)
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "workflow: add HTML report for run-id ${{ github.run_id }} (attempt: ${{ github.run_attempt }})"
|
||||
|
||||
while true; do
|
||||
git pull --rebase
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to rebase. Please review manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git push
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Successfully pushed HTML report to repo."
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
- name: Output Report URL as Workflow Annotation
|
||||
id: url
|
||||
run: |
|
||||
FULL_HTML_REPORT_URL=https://calcom.github.io/cal.com/$HTML_REPORT_URL_PATH
|
||||
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
||||
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: <!-- GENERATED-E2E-RESULTS -->
|
||||
- name: Create comment
|
||||
if: steps.fc.outputs.comment-id == ''
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
<!-- GENERATED-E2E-RESULTS -->
|
||||
## E2E results are ready!
|
||||
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
- name: Update comment
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!-- GENERATED-E2E-RESULTS -->
|
||||
## E2E results are ready!
|
||||
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
@@ -15,3 +15,4 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
|
||||
@@ -380,7 +380,7 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
|
||||
|
||||
### Elestio
|
||||
|
||||
[](https://elest.io/open-source/cal.com)
|
||||
[](https://elest.io/open-source/cal.com)
|
||||
|
||||
<!-- ROADMAP -->
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
|
||||
|
||||
// FIXME: Payment seems a delicate endpoint, do we need to remove anything here?
|
||||
export const schemaPaymentBodyParams = Payment.omit({ id: true });
|
||||
export const schemaPaymentPublic = Payment.omit({ externalId: true });
|
||||
export const schemaPaymentPublic = Payment.pick({
|
||||
id: true,
|
||||
amount: true,
|
||||
success: true,
|
||||
refunded: true,
|
||||
fee: true,
|
||||
paymentOption: true,
|
||||
currency: true,
|
||||
bookingId: true,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
|
||||
isOrganization: true,
|
||||
isPlatform: true,
|
||||
smsLockState: true,
|
||||
smsLockReviewedByAdmin: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({
|
||||
|
||||
@@ -27,7 +27,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* url: https://docs.cal.com/docs/core-features/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability removed successfully
|
||||
|
||||
@@ -28,7 +28,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* url: https://docs.cal.com/docs/core-features/availability
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
|
||||
@@ -63,7 +63,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* url: https://docs.cal.com/docs/core-features/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability edited successfully
|
||||
|
||||
@@ -62,7 +62,7 @@ import {
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* url: https://docs.cal.com/docs/core-features/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability created
|
||||
|
||||
@@ -11,8 +11,8 @@ const swaggerHandler = withSwagger({
|
||||
{ url: "https://api.cal.com/v1" },
|
||||
],
|
||||
externalDocs: {
|
||||
url: "https://docs.cal.com",
|
||||
description: "Find more info at our main docs: https://docs.cal.com/",
|
||||
url: "https://docs.cal.com/docs",
|
||||
description: "Find more info at our main docs: https://docs.cal.com/docs/",
|
||||
},
|
||||
info: {
|
||||
title: `${pjson.name}: ${pjson.description}`,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* url: https://docs.cal.com/docs/core-features/event-types
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, eventType removed successfully
|
||||
|
||||
@@ -34,7 +34,7 @@ import getCalLink from "../_utils/getCalLink";
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* url: https://docs.cal.com/docs/core-features/event-types
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
|
||||
@@ -194,7 +194,7 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* url: https://docs.cal.com/docs/core-features/event-types
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, eventType edited successfully
|
||||
|
||||
@@ -33,7 +33,7 @@ import getCalLink from "./_utils/getCalLink";
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* url: https://docs.cal.com/docs/core-features/event-types
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
|
||||
@@ -255,7 +255,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* url: https://docs.cal.com/docs/core-features/event-types
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, event type created
|
||||
|
||||
@@ -125,6 +125,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||
} = {
|
||||
...data,
|
||||
smsLockReviewedByAdmin: false,
|
||||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||
};
|
||||
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
||||
|
||||
@@ -32,7 +32,7 @@ const querySchema = z.object({
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* url: https://docs.cal.com/docs/core-features/event-types
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
|
||||
@@ -125,6 +125,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||
} = {
|
||||
...data,
|
||||
smsLockReviewedByAdmin: false,
|
||||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
* tags:
|
||||
* - webhooks
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/webhooks
|
||||
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, hook removed successfully
|
||||
|
||||
@@ -28,7 +28,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
|
||||
* tags:
|
||||
* - webhooks
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/webhooks
|
||||
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
|
||||
@@ -58,7 +58,7 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
||||
* tags:
|
||||
* - webhooks
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/webhooks
|
||||
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, webhook edited successfully
|
||||
|
||||
@@ -24,7 +24,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
|
||||
* tags:
|
||||
* - webhooks
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/webhooks
|
||||
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
|
||||
@@ -56,7 +56,7 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
|
||||
* tags:
|
||||
* - webhooks
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/webhooks
|
||||
* url: https://docs.cal.com/docs/core-features/webhooks
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, webhook created
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.28",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-libraries-0.0.23": "npm:@calcom/platform-libraries@0.0.23",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries-0.0.23";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Bookings Endpoints", () => {
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
getBookingInfo,
|
||||
handleCancelBooking,
|
||||
getBookingForReschedule,
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries-0.0.23";
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||
@IsBoolean()
|
||||
|
||||
@@ -105,7 +105,7 @@ class Primary {
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
class Calendar {
|
||||
export class Calendar {
|
||||
@IsEmail()
|
||||
externalId!: string;
|
||||
|
||||
@@ -135,7 +135,7 @@ class Calendar {
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
class ConnectedCalendar {
|
||||
export class ConnectedCalendar {
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
integration!: Integration;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.23";
|
||||
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class AppleCalendarService implements CredentialSyncCalendarApp {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { User } from "@prisma/client";
|
||||
import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.23";
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
|
||||
import { Calendar } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import {
|
||||
EventTypesPublic,
|
||||
eventTypeBookingFields,
|
||||
eventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getEventTypeById } from "@calcom/platform-libraries-0.0.23";
|
||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+4
@@ -177,6 +177,10 @@ export class BookingField_2024_04_15 {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
export class RecurringEvent_2024_04_15 {
|
||||
|
||||
+4
@@ -87,6 +87,10 @@ class BookingField {
|
||||
@Type(() => Source)
|
||||
@IsOptional()
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
class Organization {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
updateEventType,
|
||||
EventTypesPublic,
|
||||
getEventTypesPublic,
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+388
-1
@@ -1,5 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
@@ -25,6 +26,8 @@ import {
|
||||
EventTypeOutput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} 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";
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
describe("Not authenticated", () => {
|
||||
@@ -212,6 +215,21 @@ describe("Event types Endpoints", () => {
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
bookingLimitsCount: {
|
||||
day: 2,
|
||||
week: 5,
|
||||
},
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
bookingLimitsDuration: {
|
||||
day: 60,
|
||||
week: 100,
|
||||
},
|
||||
offsetStart: 30,
|
||||
bookingWindow: {
|
||||
type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays,
|
||||
value: 30,
|
||||
rolling: true,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -230,7 +248,11 @@ describe("Event types Endpoints", () => {
|
||||
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
|
||||
expect(createdEventType.ownerId).toEqual(user.id);
|
||||
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
|
||||
|
||||
expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
expect(createdEventType.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
|
||||
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
|
||||
eventType = responseBody.data;
|
||||
});
|
||||
});
|
||||
@@ -241,6 +263,21 @@ describe("Event types Endpoints", () => {
|
||||
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||
title: newTitle,
|
||||
scheduleId: secondSchedule.id,
|
||||
bookingLimitsCount: {
|
||||
day: 4,
|
||||
week: 10,
|
||||
},
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
bookingLimitsDuration: {
|
||||
day: 100,
|
||||
week: 200,
|
||||
},
|
||||
offsetStart: 50,
|
||||
bookingWindow: {
|
||||
type: BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays,
|
||||
value: 40,
|
||||
rolling: false,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -261,9 +298,19 @@ describe("Event types Endpoints", () => {
|
||||
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(updatedEventType.ownerId).toEqual(user.id);
|
||||
expect(updatedEventType.scheduleId).toEqual(secondSchedule.id);
|
||||
expect(updatedEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
expect(updatedEventType.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
|
||||
expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow);
|
||||
|
||||
eventType.title = newTitle;
|
||||
eventType.scheduleId = secondSchedule.id;
|
||||
eventType.bookingLimitsCount = updatedEventType.bookingLimitsCount;
|
||||
eventType.onlyShowFirstAvailableSlot = updatedEventType.onlyShowFirstAvailableSlot;
|
||||
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
|
||||
eventType.offsetStart = updatedEventType.offsetStart;
|
||||
eventType.bookingWindow = updatedEventType.bookingWindow;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,6 +346,11 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
});
|
||||
|
||||
it(`/GET/even-types by username`, async () => {
|
||||
@@ -324,6 +376,11 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and eventSlug`, async () => {
|
||||
@@ -344,6 +401,11 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
});
|
||||
|
||||
it(`/GET/:id not existing`, async () => {
|
||||
@@ -380,4 +442,329 @@ describe("Event types Endpoints", () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle legacy event-types booking fields", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "legacy-event-types-test-e2e@api.com";
|
||||
const name = "bob-the-builder";
|
||||
const username = name;
|
||||
let user: User;
|
||||
let legacyEventTypeId1: number;
|
||||
let legacyEventTypeId2: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
name,
|
||||
username,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(oAuthClient).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not allow creating an event type with input of event-types version 2024_04_15", async () => {
|
||||
const body: CreateEventTypeInput_2024_04_15 = {
|
||||
title: "Coding class",
|
||||
slug: "coding-class",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
length: 60,
|
||||
locations: [{ type: "integrations:daily" }],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return empty bookingFields if system fields are the only one in database", async () => {
|
||||
const legacyEventTypeInput = {
|
||||
title: "legacy event type",
|
||||
description: "legacy event type description",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "legacy-event-type",
|
||||
locations: [],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "your_name",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "email_address",
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
type: "radioInput",
|
||||
label: "",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: false,
|
||||
placeholder: "",
|
||||
defaultLabel: "location",
|
||||
getOptionsAt: "locations",
|
||||
optionsInputs: {
|
||||
phone: { type: "phone", required: true, placeholder: "" },
|
||||
attendeeInPerson: { type: "address", required: true, placeholder: "" },
|
||||
},
|
||||
hideWhenJustOneOption: true,
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
hidden: true,
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: true,
|
||||
defaultLabel: "what_is_this_meeting_about",
|
||||
defaultPlaceholder: "",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
{
|
||||
name: "guests",
|
||||
type: "multiemail",
|
||||
hidden: false,
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_guests",
|
||||
defaultPlaceholder: "email",
|
||||
},
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
views: [{ id: "reschedule", label: "Reschedule View" }],
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
legacyEventTypeId1 = legacyEventType.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${legacyEventTypeId1}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
expect(fetchedEventType.bookingFields).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return user created bookingFields among system fields in the database", async () => {
|
||||
const userDefinedBookingField = {
|
||||
name: "team",
|
||||
type: "textarea",
|
||||
label: "your team",
|
||||
sources: [
|
||||
{
|
||||
id: "user",
|
||||
type: "user",
|
||||
label: "User",
|
||||
fieldRequired: true,
|
||||
},
|
||||
],
|
||||
editable: "user",
|
||||
required: true,
|
||||
placeholder: "FC Barcelona",
|
||||
};
|
||||
|
||||
const legacyEventTypeInput = {
|
||||
title: "legacy event type two",
|
||||
description: "legacy event type description two",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "legacy-event-type-two",
|
||||
locations: [],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [
|
||||
userDefinedBookingField,
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "your_name",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "email_address",
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
type: "radioInput",
|
||||
label: "",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: false,
|
||||
placeholder: "",
|
||||
defaultLabel: "location",
|
||||
getOptionsAt: "locations",
|
||||
optionsInputs: {
|
||||
phone: { type: "phone", required: true, placeholder: "" },
|
||||
attendeeInPerson: { type: "address", required: true, placeholder: "" },
|
||||
},
|
||||
hideWhenJustOneOption: true,
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
hidden: true,
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: true,
|
||||
defaultLabel: "what_is_this_meeting_about",
|
||||
defaultPlaceholder: "",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
{
|
||||
name: "guests",
|
||||
type: "multiemail",
|
||||
hidden: false,
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_guests",
|
||||
defaultPlaceholder: "email",
|
||||
},
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
views: [{ id: "reschedule", label: "Reschedule View" }],
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
legacyEventTypeId2 = legacyEventType.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${legacyEventTypeId2}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{
|
||||
type: userDefinedBookingField.type,
|
||||
slug: userDefinedBookingField.name,
|
||||
label: userDefinedBookingField.label,
|
||||
required: userDefinedBookingField.required,
|
||||
placeholder: userDefinedBookingField.placeholder,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
try {
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
||||
} catch (e) {
|
||||
// Event type might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(user.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,13 +7,24 @@ import {
|
||||
getEventTypeById,
|
||||
transformApiEventTypeBookingFields,
|
||||
transformApiEventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type InputEventTransformed = Omit<
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
"lengthInMinutes" | "locations" | "bookingFields"
|
||||
| "lengthInMinutes"
|
||||
| "locations"
|
||||
| "bookingFields"
|
||||
| "bookingLimitsCount"
|
||||
| "onlyShowFirstAvailableSlot"
|
||||
| "bookingLimitsDuration"
|
||||
| "offsetStart"
|
||||
| "periodType"
|
||||
| "periodDays"
|
||||
| "periodCountCalendarDays"
|
||||
| "periodStartDate"
|
||||
| "periodEndDate"
|
||||
> & {
|
||||
length: number;
|
||||
slug: string;
|
||||
|
||||
+36
-4
@@ -10,9 +10,9 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.23";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.23";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries-0.0.23";
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
@@ -38,7 +38,16 @@ export class EventTypesService_2024_06_14 {
|
||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const bodyTransformed = this.inputEventTypesService.transformInputCreateEventType(body);
|
||||
const {
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
periodType = undefined,
|
||||
periodDays = undefined,
|
||||
periodCountCalendarDays = undefined,
|
||||
periodStartDate = undefined,
|
||||
periodEndDate = undefined,
|
||||
...bodyTransformed
|
||||
} = this.inputEventTypesService.transformInputCreateEventType(body);
|
||||
const { eventType: eventTypeCreated } = await createEventType({
|
||||
input: bodyTransformed,
|
||||
ctx: {
|
||||
@@ -49,6 +58,26 @@ export class EventTypesService_2024_06_14 {
|
||||
},
|
||||
});
|
||||
|
||||
await updateEventType({
|
||||
input: {
|
||||
id: eventTypeCreated.id,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
periodType,
|
||||
periodDays,
|
||||
periodCountCalendarDays,
|
||||
periodStartDate,
|
||||
periodEndDate,
|
||||
...bodyTransformed,
|
||||
},
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbWrite.prisma,
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id);
|
||||
|
||||
if (!eventType) {
|
||||
@@ -95,13 +124,16 @@ export class EventTypesService_2024_06_14 {
|
||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||
: false;
|
||||
const profileId = user.movedToProfile?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
return {
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
username: user.username,
|
||||
organizationId: user.organizationId,
|
||||
organization: { isOrgAdmin },
|
||||
profile: { id: profileId },
|
||||
metadata: user.metadata,
|
||||
selectedCalendars,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+41
-3
@@ -3,7 +3,9 @@ import { Injectable } from "@nestjs/common";
|
||||
import {
|
||||
transformApiEventTypeBookingFields,
|
||||
transformApiEventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
transformApiEventTypeIntervalLimits,
|
||||
transformApiEventTypeFutureBookingLimits,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
@@ -16,20 +18,42 @@ export class InputEventTypesService_2024_06_14 {
|
||||
},
|
||||
];
|
||||
|
||||
const { lengthInMinutes, locations, bookingFields, ...rest } = inputEventType;
|
||||
const {
|
||||
lengthInMinutes,
|
||||
locations,
|
||||
bookingFields,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
...rest
|
||||
} = inputEventType;
|
||||
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: this.transformInputLocations(locations || defaultLocations),
|
||||
bookingFields: this.transformInputBookingFields(bookingFields),
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
: undefined,
|
||||
...this.transformInputBookingWindow(bookingWindow),
|
||||
};
|
||||
|
||||
return eventType;
|
||||
}
|
||||
|
||||
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
|
||||
const { lengthInMinutes, locations, bookingFields, scheduleId, ...rest } = inputEventType;
|
||||
const {
|
||||
lengthInMinutes,
|
||||
locations,
|
||||
bookingFields,
|
||||
scheduleId,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
...rest
|
||||
} = inputEventType;
|
||||
|
||||
const eventType = {
|
||||
...rest,
|
||||
@@ -37,6 +61,11 @@ export class InputEventTypesService_2024_06_14 {
|
||||
locations: locations ? this.transformInputLocations(locations) : undefined,
|
||||
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
|
||||
schedule: scheduleId,
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
: undefined,
|
||||
...this.transformInputBookingWindow(bookingWindow),
|
||||
};
|
||||
|
||||
return eventType;
|
||||
@@ -49,4 +78,13 @@ export class InputEventTypesService_2024_06_14 {
|
||||
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
|
||||
return transformApiEventTypeBookingFields(inputBookingFields);
|
||||
}
|
||||
|
||||
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
|
||||
return transformApiEventTypeIntervalLimits(inputBookingFields);
|
||||
}
|
||||
|
||||
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
|
||||
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
|
||||
return !!res ? res : {};
|
||||
}
|
||||
}
|
||||
|
||||
+47
-4
@@ -9,7 +9,13 @@ import {
|
||||
parseRecurringEvent,
|
||||
TransformedLocationsSchema,
|
||||
BookingFieldsSchema,
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
SystemField,
|
||||
UserField,
|
||||
parseBookingLimit,
|
||||
getResponseEventTypeIntervalLimits,
|
||||
getResponseEventTypeFutureBookingLimits,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
|
||||
type DatabaseEventType = EventType & EventTypeRelations;
|
||||
@@ -42,6 +48,15 @@ type Input = Pick<
|
||||
| "metadata"
|
||||
| "users"
|
||||
| "scheduleId"
|
||||
| "bookingLimits"
|
||||
| "durationLimits"
|
||||
| "onlyShowFirstAvailableSlot"
|
||||
| "offsetStart"
|
||||
| "periodType"
|
||||
| "periodDays"
|
||||
| "periodCountCalendarDays"
|
||||
| "periodStartDate"
|
||||
| "periodEndDate"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -69,13 +84,26 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
seatsShowAvailabilityCount,
|
||||
isInstantEvent,
|
||||
scheduleId,
|
||||
onlyShowFirstAvailableSlot,
|
||||
offsetStart,
|
||||
} = databaseEventType;
|
||||
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
const bookingFields = this.transformBookingFields(databaseEventType.bookingFields);
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
|
||||
: [];
|
||||
const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
const users = this.transformUsers(databaseEventType.users);
|
||||
const bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits);
|
||||
const bookingLimitsDuration = this.transformIntervalLimits(databaseEventType.durationLimits);
|
||||
const bookingWindow = this.transformBookingWindow({
|
||||
periodType: databaseEventType.periodType,
|
||||
periodDays: databaseEventType.periodDays,
|
||||
periodCountCalendarDays: databaseEventType.periodCountCalendarDays,
|
||||
periodStartDate: databaseEventType.periodStartDate,
|
||||
periodEndDate: databaseEventType.periodEndDate,
|
||||
} as TransformFutureBookingsLimitSchema_2024_06_14);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -105,6 +133,11 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
isInstantEvent,
|
||||
users,
|
||||
scheduleId,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
onlyShowFirstAvailableSlot,
|
||||
offsetStart,
|
||||
bookingWindow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,9 +146,10 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
|
||||
}
|
||||
|
||||
transformBookingFields(inputBookingFields: any) {
|
||||
transformBookingFields(inputBookingFields: (SystemField | UserField)[] | null) {
|
||||
if (!inputBookingFields) return [];
|
||||
return getResponseEventTypeBookingFields(BookingFieldsSchema.parse(inputBookingFields));
|
||||
const userFields = inputBookingFields.filter((field) => field.editable === "user") as UserField[];
|
||||
return getResponseEventTypeBookingFields(userFields);
|
||||
}
|
||||
|
||||
transformRecurringEvent(recurringEvent: any) {
|
||||
@@ -143,4 +177,13 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
transformIntervalLimits(bookingLimits: any) {
|
||||
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
|
||||
return getResponseEventTypeIntervalLimits(bookingLimitsParsed);
|
||||
}
|
||||
|
||||
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
|
||||
return getResponseEventTypeFutureBookingLimits(bookingLimits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import { User, Team } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
@@ -25,9 +27,11 @@ describe("Me Endpoints", () => {
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
const userEmail = "me-controller-e2e@api.com";
|
||||
let user: User;
|
||||
let org: Team;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -43,6 +47,9 @@ describe("Me Endpoints", () => {
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
@@ -50,6 +57,20 @@ describe("Me Endpoints", () => {
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test org team",
|
||||
isOrganization: true,
|
||||
isPlatform: true,
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: userEmail,
|
||||
user: { connect: { id: user.id } },
|
||||
organization: { connect: { id: org.id } },
|
||||
movedFromUser: { connect: { id: user.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
@@ -75,6 +96,8 @@ describe("Me Endpoints", () => {
|
||||
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
|
||||
expect(responseBody.data.weekStart).toEqual(user.weekStart);
|
||||
expect(responseBody.data.timeZone).toEqual(user.timeZone);
|
||||
expect(responseBody.data.organization?.isPlatform).toEqual(true);
|
||||
expect(responseBody.data.organization?.id).toEqual(org.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,6 +161,7 @@ describe("Me Endpoints", () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
@@ -23,14 +24,26 @@ import { userSchemaResponse } from "@calcom/platform-types";
|
||||
export class MeController {
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly schedulesService: SchedulesService_2024_04_15
|
||||
private readonly schedulesService: SchedulesService_2024_04_15,
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@Permissions([PROFILE_READ])
|
||||
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
|
||||
const me = userSchemaResponse.parse(user);
|
||||
|
||||
const organization = this.usersService.getUserMainProfile(user)?.organization;
|
||||
const me = userSchemaResponse.parse(
|
||||
organization
|
||||
? {
|
||||
...user,
|
||||
organizationId: organization.id,
|
||||
organization: {
|
||||
id: organization.id,
|
||||
isPlatform: organization.isPlatform,
|
||||
},
|
||||
}
|
||||
: user
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: me,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { IsInt, IsEmail, IsOptional, IsString } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
export class MeOrgOutput {
|
||||
isPlatform!: boolean;
|
||||
|
||||
id!: number;
|
||||
}
|
||||
export class MeOutput {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
@@ -25,4 +31,9 @@ export class MeOutput {
|
||||
|
||||
@IsInt()
|
||||
organizationId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => MeOrgOutput)
|
||||
organization?: MeOrgOutput;
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
transformApiScheduleOverrides,
|
||||
transformApiScheduleAvailability,
|
||||
} from "@calcom/platform-libraries-0.0.23";
|
||||
import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries";
|
||||
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { TRPCError } from "@calcom/platform-libraries-0.0.23";
|
||||
import { TRPCError } from "@calcom/platform-libraries";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
@Catch(TRPCError)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
import { hasPermissions } from "@calcom/platform-utils";
|
||||
|
||||
@@ -24,6 +25,12 @@ export class PermissionsGuard implements CanActivate {
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authString = request.get("Authorization")?.replace("Bearer ", "");
|
||||
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
|
||||
|
||||
if (nextAuthToken) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!authString) {
|
||||
return false;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Injectable, InternalServerErrorException, UnauthorizedException } from
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import type { Request } from "express";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
@@ -45,6 +46,13 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
return await this.authenticateBearerToken(bearerToken, requestOrigin);
|
||||
}
|
||||
|
||||
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
|
||||
|
||||
if (nextAuthToken) {
|
||||
return await this.authenticateNextAuth(nextAuthToken);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(
|
||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers"
|
||||
);
|
||||
@@ -58,6 +66,11 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateNextAuth(token: { email?: string | null }) {
|
||||
const user = await this.nextAuthStrategy(token);
|
||||
return this.success(user);
|
||||
}
|
||||
|
||||
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
||||
return this.success(user);
|
||||
@@ -163,4 +176,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||
return user;
|
||||
}
|
||||
|
||||
async nextAuthStrategy(token: { email?: string | null }) {
|
||||
if (!token.email) {
|
||||
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findByEmailWithProfile(token.email);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { DestinationCalendarsOutputResponseDto } from "@/modules/destination-calendars/outputs/destination-calendars.output";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User, Credential } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
|
||||
import { APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||
|
||||
describe("Platform Destination Calendar Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
|
||||
let appleCalendarCredentials: Credential;
|
||||
let user: User;
|
||||
let accessTokenSecret: string;
|
||||
let refreshTokenSecret: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, TokensModule],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
|
||||
.compile();
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
|
||||
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
|
||||
accessTokenSecret = tokens.accessToken;
|
||||
refreshTokenSecret = tokens.refreshToken;
|
||||
appleCalendarCredentials = await credentialsRepositoryFixture.create(
|
||||
APPLE_CALENDAR_TYPE,
|
||||
{},
|
||||
user.id,
|
||||
APPLE_CALENDAR_ID
|
||||
);
|
||||
jest.spyOn(CalendarsService.prototype, "getCalendars").mockReturnValue(
|
||||
Promise.resolve({
|
||||
connectedCalendars: [
|
||||
{
|
||||
integration: {
|
||||
installed: false,
|
||||
type: "apple_calendar",
|
||||
title: "",
|
||||
name: "",
|
||||
description: "",
|
||||
variant: "calendar",
|
||||
slug: "",
|
||||
locationOption: null,
|
||||
categories: ["calendar"],
|
||||
logo: "",
|
||||
publisher: "",
|
||||
url: "",
|
||||
email: "",
|
||||
},
|
||||
calendars: {
|
||||
externalId:
|
||||
"https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||
readOnly: false,
|
||||
integration: "apple_calendar",
|
||||
credentialId: appleCalendarCredentials.id,
|
||||
primary: true,
|
||||
email: user.email,
|
||||
},
|
||||
error: { message: "" },
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: [CLIENT_REDIRECT_URI],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(oAuthClient).toBeDefined();
|
||||
expect(accessTokenSecret).toBeDefined();
|
||||
expect(refreshTokenSecret).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it(`POST /v2/destination-calendars: it should respond with a 200 returning back the user updated destination calendar`, async () => {
|
||||
const body = {
|
||||
integration: appleCalendarCredentials.type,
|
||||
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.put("/v2/destination-calendars")
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: DestinationCalendarsOutputResponseDto = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.credentialId).toEqual(appleCalendarCredentials.id);
|
||||
expect(responseBody.data.integration).toEqual(body.integration);
|
||||
expect(responseBody.data.externalId).toEqual(body.externalId);
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
it(`POST /v2/destination-calendars: should fail 400 if calendar type is invalid`, async () => {
|
||||
const body = {
|
||||
integration: "not-supported-calendar",
|
||||
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.put("/v2/destination-calendars")
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { DestinationCalendarsInputBodyDto } from "@/modules/destination-calendars/inputs/destination-calendars.input";
|
||||
import {
|
||||
DestinationCalendarsOutputDto,
|
||||
DestinationCalendarsOutputResponseDto,
|
||||
} from "@/modules/destination-calendars/outputs/destination-calendars.output";
|
||||
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Body, Controller, Put, UseGuards } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/destination-calendars",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Destination-Calendars")
|
||||
export class DestinationCalendarsController {
|
||||
constructor(private readonly destinationCalendarsService: DestinationCalendarsService) {}
|
||||
|
||||
@Put("/")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@DocsTags("Select a third party destination calendar where events will be created")
|
||||
async updateDestinationCalendars(
|
||||
@Body() input: DestinationCalendarsInputBodyDto,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<DestinationCalendarsOutputResponseDto> {
|
||||
const { integration, externalId } = input;
|
||||
const updatedDestinationCalendar = await this.destinationCalendarsService.updateDestinationCalendars(
|
||||
integration,
|
||||
externalId,
|
||||
user.id
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(DestinationCalendarsOutputDto, updatedDestinationCalendar, {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { DestinationCalendarsController } from "@/modules/destination-calendars/controllers/destination-calendars.controller";
|
||||
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
|
||||
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [
|
||||
CalendarsRepository,
|
||||
CalendarsService,
|
||||
DestinationCalendarsService,
|
||||
DestinationCalendarsRepository,
|
||||
UsersRepository,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
],
|
||||
controllers: [DestinationCalendarsController],
|
||||
exports: [DestinationCalendarsRepository],
|
||||
})
|
||||
export class DestinationCalendarsModule {}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class DestinationCalendarsRepository {
|
||||
constructor(private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async updateCalendar(
|
||||
integration: string,
|
||||
externalId: string,
|
||||
credentialId: number,
|
||||
userId: number,
|
||||
primaryEmail: string | null
|
||||
) {
|
||||
return await this.dbWrite.prisma.destinationCalendar.upsert({
|
||||
update: {
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
primaryEmail,
|
||||
},
|
||||
create: {
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
primaryEmail,
|
||||
userId,
|
||||
},
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsString, IsEnum } from "class-validator";
|
||||
|
||||
import {
|
||||
APPLE_CALENDAR_TYPE,
|
||||
GOOGLE_CALENDAR_TYPE,
|
||||
OFFICE_365_CALENDAR_TYPE,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
export class DestinationCalendarsInputBodyDto {
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
example: APPLE_CALENDAR_TYPE,
|
||||
description: "The calendar service you want to integrate, as returned by the /calendars endpoint",
|
||||
enum: [APPLE_CALENDAR_TYPE, GOOGLE_CALENDAR_TYPE, OFFICE_365_CALENDAR_TYPE],
|
||||
required: true,
|
||||
})
|
||||
@IsEnum([APPLE_CALENDAR_TYPE, GOOGLE_CALENDAR_TYPE, OFFICE_365_CALENDAR_TYPE])
|
||||
readonly integration!:
|
||||
| typeof APPLE_CALENDAR_TYPE
|
||||
| typeof GOOGLE_CALENDAR_TYPE
|
||||
| typeof OFFICE_365_CALENDAR_TYPE;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
example: "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
|
||||
description:
|
||||
"Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint",
|
||||
type: "string",
|
||||
required: true,
|
||||
})
|
||||
readonly externalId!: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsString, ValidateNested, IsEnum } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class DestinationCalendarsOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly integration!: string;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly externalId!: string;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly credentialId!: number | null;
|
||||
}
|
||||
|
||||
export class DestinationCalendarsOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => DestinationCalendarsOutputDto)
|
||||
data!: DestinationCalendarsOutputDto;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { ConnectedCalendar, Calendar } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class DestinationCalendarsService {
|
||||
constructor(
|
||||
private readonly calendarsService: CalendarsService,
|
||||
private readonly destinationCalendarsRepository: DestinationCalendarsRepository
|
||||
) {}
|
||||
|
||||
async updateDestinationCalendars(integration: string, externalId: string, userId: number) {
|
||||
const userCalendars = await this.calendarsService.getCalendars(userId);
|
||||
const allCalendars = userCalendars.connectedCalendars
|
||||
.map((cal: ConnectedCalendar) => cal.calendars ?? [])
|
||||
.flat();
|
||||
const credentialId = allCalendars.find(
|
||||
(cal: Calendar) =>
|
||||
cal.externalId === externalId && cal.integration === integration && cal.readOnly === false
|
||||
)?.credentialId;
|
||||
|
||||
if (!credentialId) {
|
||||
throw new NotFoundException(`Could not find calendar ${externalId}`);
|
||||
}
|
||||
|
||||
const primaryEmail =
|
||||
allCalendars.find((cal: Calendar) => cal.primary && cal.credentialId === credentialId)?.email ?? null;
|
||||
|
||||
const {
|
||||
integration: updatedCalendarIntegration,
|
||||
externalId: updatedCalendarExternalId,
|
||||
credentialId: updatedCalendarCredentialId,
|
||||
userId: updatedCalendarUserId,
|
||||
} = await this.destinationCalendarsRepository.updateCalendar(
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
userId,
|
||||
primaryEmail
|
||||
);
|
||||
|
||||
return {
|
||||
userId: updatedCalendarUserId,
|
||||
integration: updatedCalendarIntegration,
|
||||
externalId: updatedCalendarExternalId,
|
||||
credentialId: updatedCalendarCredentialId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.23";
|
||||
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
@@ -16,6 +17,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
TimezoneModule,
|
||||
UsersModule,
|
||||
WebhooksModule,
|
||||
DestinationCalendarsModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
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 {
|
||||
OAuthClientWebhooksOutputResponseDto,
|
||||
OAuthClientWebhookOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||
import { withNextAuth } from "test/utils/withNextAuth";
|
||||
|
||||
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
|
||||
|
||||
describe("EventTypes WebhooksController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "event-types-webhook-controller-e2e@api.com";
|
||||
const otherUserEmail = "other-event-types-webhook-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let otherUser: UserWithProfile;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let otherOAuthClient: PlatformOAuthClient;
|
||||
let org: Team;
|
||||
let otherOrg: Team;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let webhookRepositoryFixture: WebhookRepositoryFixture;
|
||||
let otherOAuthClientWebhook: Webhook;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let orgRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
|
||||
let webhook: OAuthClientWebhookOutputResponseDto["data"];
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withNextAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
otherUser = await userRepositoryFixture.create({
|
||||
email: otherUserEmail,
|
||||
username: otherUserEmail,
|
||||
});
|
||||
|
||||
org = await orgRepositoryFixture.create({
|
||||
name: "apiOrg",
|
||||
isOrganization: true,
|
||||
metadata: {
|
||||
isOrganization: true,
|
||||
orgAutoAcceptEmail: "api.com",
|
||||
isOrganizationVerified: true,
|
||||
isOrganizationConfigured: true,
|
||||
},
|
||||
isPlatform: true,
|
||||
});
|
||||
otherOrg = await orgRepositoryFixture.create({
|
||||
name: "otherOrg",
|
||||
isOrganization: true,
|
||||
metadata: {
|
||||
isOrganization: true,
|
||||
orgAutoAcceptEmail: "api.com",
|
||||
isOrganizationVerified: true,
|
||||
isOrganizationConfigured: true,
|
||||
},
|
||||
isPlatform: true,
|
||||
});
|
||||
await platformBillingRepositoryFixture.create(org.id);
|
||||
await platformBillingRepositoryFixture.create(otherOrg.id);
|
||||
await membershipRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
accepted: true,
|
||||
});
|
||||
await membershipRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: otherUser.id } },
|
||||
team: { connect: { id: otherOrg.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
movedFromUser: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: { id: user.id },
|
||||
},
|
||||
});
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${otherUser.id}`,
|
||||
username: otherUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: otherOrg.id,
|
||||
},
|
||||
},
|
||||
movedFromUser: {
|
||||
connect: {
|
||||
id: otherUser.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: { id: otherUser.id },
|
||||
},
|
||||
});
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
oAuthClient = await oAuthClientRepositoryFixture.create(org.id, data, secret);
|
||||
otherOAuthClient = await oAuthClientRepositoryFixture.create(otherOrg.id, data, secret);
|
||||
otherOAuthClientWebhook = await webhookRepositoryFixture.create({
|
||||
id: "123abc-123abc-123abc-123abc",
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
subscriberUrl: "https://example.com",
|
||||
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
platformOAuthClient: { connect: { id: otherOAuthClient.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await orgRepositoryFixture.delete(org.id);
|
||||
await userRepositoryFixture.deleteByEmail(otherUser.email);
|
||||
await orgRepositoryFixture.delete(otherOrg.id);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/oauth-clients/${oAuthClient.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) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
active: true,
|
||||
oAuthClientId: oAuthClient.id,
|
||||
payloadTemplate: "string",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
},
|
||||
} satisfies OAuthClientWebhookOutputResponseDto);
|
||||
webhook = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (PATCH)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/oauth-clients/${oAuthClient.id}/webhooks/${webhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.data.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/oauth-clients/${oAuthClient.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",
|
||||
oAuthClientId: oAuthClient.id,
|
||||
},
|
||||
} satisfies OAuthClientWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/oauth-clients/${oAuthClient.id}/webhooks/90284`).expect(404);
|
||||
});
|
||||
|
||||
it("/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/oauth-clients/${oAuthClient.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as OAuthClientWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.oAuthClientId).toBe(oAuthClient.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
it("/webhooks (GET) should fail to get webhooks of oauth client that doesn't belong to you", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/oauth-clients/${otherOAuthClient.id}/webhooks`).expect(403);
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/oauth-clients/${oAuthClient.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",
|
||||
oAuthClientId: oAuthClient.id,
|
||||
},
|
||||
} satisfies OAuthClientWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) should fail to delete webhooks of an oauth client that doesn't belong to you", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/oauth-clients/${otherOAuthClient.id}/webhooks/${otherOAuthClientWebhook.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/oauth-clients/${oAuthClient.id}/webhooks/1234453`)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
||||
import { IsOAuthClientWebhookGuard } from "@/modules/webhooks/guards/is-oauth-client-webhook-guard";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
OAuthClientWebhookOutputResponseDto,
|
||||
OAuthClientWebhookOutputDto,
|
||||
OAuthClientWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
|
||||
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
|
||||
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||
import { OAuthClientWebhooksService } from "@/modules/webhooks/services/oauth-clients-webhooks.service";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Webhook, MembershipRole } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
import { OAuthClientGuard } from "../../guards/oauth-client-guard";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/oauth-clients/:clientId/webhooks",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard, OAuthClientGuard)
|
||||
@ApiTags("OAuthClients Webhooks")
|
||||
export class OAuthClientWebhooksController {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly oAuthClientWebhooksService: OAuthClientWebhooksService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Create a webhook for an oAuthClient" })
|
||||
async createOAuthClientWebhook(
|
||||
@Body() body: CreateWebhookInputDto,
|
||||
@Param("clientId") oAuthClientId: string
|
||||
): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
const webhook = await this.oAuthClientWebhooksService.createOAuthClientWebhook(
|
||||
oAuthClientId,
|
||||
new WebhookInputPipe().transform(body)
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:webhookId")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Update a webhook of an oAuthClient" })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async updateOAuthClientWebhook(
|
||||
@Body() body: UpdateWebhookInputDto,
|
||||
@Param("webhookId") webhookId: string
|
||||
): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.updateWebhook(
|
||||
webhookId,
|
||||
new PartialWebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:webhookId")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@ApiOperation({ summary: "Get a webhook of an oAuthClient" })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async getOAuthClientWebhook(@GetWebhook() webhook: Webhook): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@ApiOperation({ summary: "Get all webhooks of an oAuthClient" })
|
||||
async getOAuthClientWebhooks(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Query() pagination: SkipTakePagination
|
||||
): Promise<OAuthClientWebhooksOutputResponseDto> {
|
||||
const webhooks = await this.oAuthClientWebhooksService.getOAuthClientWebhooksPaginated(
|
||||
oAuthClientId,
|
||||
pagination.skip ?? 0,
|
||||
pagination.take ?? 250
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: webhooks.map((webhook) =>
|
||||
plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:webhookId")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Delete a webhook of an oAuthClient" })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async deleteOAuthClientWebhook(
|
||||
@GetWebhook() webhook: Webhook
|
||||
): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
await this.webhooksService.deleteWebhook(webhook.id);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Delete all webhooks of an oAuthClient" })
|
||||
async deleteAllOAuthClientWebhooks(
|
||||
@Param("clientId") oAuthClientId: string
|
||||
): Promise<DeleteManyWebhooksOutputResponseDto> {
|
||||
const data = await this.oAuthClientWebhooksService.deleteAllOAuthClientWebhooks(oAuthClientId);
|
||||
return { status: SUCCESS_STATUS, data: `${data.count} webhooks deleted` };
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -37,8 +37,7 @@ import {
|
||||
ApiOperation as DocsOperation,
|
||||
ApiCreatedResponse as DocsCreatedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { User } from "@prisma/client";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
@@ -86,6 +85,7 @@ export class OAuthClientsController {
|
||||
}
|
||||
|
||||
const { id, secret } = await this.oauthClientRepository.createOAuthClient(organizationId, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
@@ -116,6 +116,7 @@ export class OAuthClientsController {
|
||||
if (!client) {
|
||||
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
|
||||
}
|
||||
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
@@ -149,6 +150,7 @@ export class OAuthClientsController {
|
||||
): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`For client ${clientId} updating OAuth Client with data: ${JSON.stringify(body)}`);
|
||||
const client = await this.oauthClientRepository.updateOAuthClient(clientId, body);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.23";
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
|
||||
+21
@@ -24,6 +24,7 @@ import {
|
||||
TeamEventTypeOutput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
} 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";
|
||||
|
||||
describe("Organizations Event Types Endpoints", () => {
|
||||
@@ -260,6 +261,21 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
userId: teammate2.id,
|
||||
},
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
day: 2,
|
||||
week: 5,
|
||||
},
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
bookingLimitsDuration: {
|
||||
day: 60,
|
||||
week: 100,
|
||||
},
|
||||
offsetStart: 30,
|
||||
bookingWindow: {
|
||||
type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays,
|
||||
value: 30,
|
||||
rolling: true,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -276,6 +292,11 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(data.schedulingType).toEqual("COLLECTIVE");
|
||||
evaluateHost(body.hosts[0], data.hosts[0]);
|
||||
evaluateHost(body.hosts[1], data.hosts[1]);
|
||||
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
expect(data.offsetStart).toEqual(body.offsetStart);
|
||||
expect(data.bookingWindow).toEqual(body.bookingWindow);
|
||||
|
||||
collectiveEventType = responseBody.data;
|
||||
});
|
||||
|
||||
@@ -23,7 +23,15 @@ export class InputOrganizationsEventTypesService {
|
||||
teamId: number,
|
||||
inputEventType: CreateTeamEventTypeInput_2024_06_14
|
||||
) {
|
||||
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
|
||||
const {
|
||||
hosts,
|
||||
assignAllTeamMembers,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
bookingFields,
|
||||
...rest
|
||||
} = inputEventType;
|
||||
|
||||
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
|
||||
|
||||
@@ -35,6 +43,10 @@ export class InputOrganizationsEventTypesService {
|
||||
? await this.getAllTeamMembers(teamId, inputEventType.schedulingType)
|
||||
: this.transformInputHosts(hosts, inputEventType.schedulingType),
|
||||
assignAllTeamMembers,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
bookingFields,
|
||||
metadata,
|
||||
};
|
||||
|
||||
|
||||
+27
-8
@@ -6,10 +6,11 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
|
||||
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.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 { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.23";
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
@@ -25,7 +26,8 @@ export class OrganizationsEventTypesService {
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly outputService: OutputOrganizationsEventTypesService,
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
async createTeamEventType(
|
||||
@@ -36,11 +38,19 @@ export class OrganizationsEventTypesService {
|
||||
) {
|
||||
await this.validateHosts(teamId, body.hosts);
|
||||
const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { hosts, assignAllTeamMembers, ...rest } =
|
||||
await this.inputService.transformInputCreateTeamEventType(teamId, body);
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
hosts,
|
||||
assignAllTeamMembers,
|
||||
locations,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
bookingFields,
|
||||
...rest
|
||||
} = await this.inputService.transformInputCreateTeamEventType(teamId, body);
|
||||
const { eventType: eventTypeCreated } = await createEventType({
|
||||
input: { teamId: teamId, ...rest },
|
||||
input: { teamId: teamId, locations, ...rest },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -52,7 +62,15 @@ export class OrganizationsEventTypesService {
|
||||
return await this.updateTeamEventType(
|
||||
eventTypeCreated.id,
|
||||
teamId,
|
||||
{ hosts: body.hosts, assignAllTeamMembers },
|
||||
{
|
||||
hosts: body.hosts,
|
||||
assignAllTeamMembers,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
bookingFields,
|
||||
...rest,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
@@ -77,7 +95,8 @@ export class OrganizationsEventTypesService {
|
||||
|
||||
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
||||
const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId);
|
||||
const profileId = user.movedToProfileId || null;
|
||||
const profileId =
|
||||
this.usersService.getUserProfileByOrgId(user, organizationId)?.id || user.movedToProfileId;
|
||||
return {
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
|
||||
@@ -3,9 +3,9 @@ import { OrganizationsEventTypesRepository } from "@/modules/organizations/repos
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { EventType, User, Schedule, Host } from "@prisma/client";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
|
||||
import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null; hosts: Host[] };
|
||||
type DatabaseEventType = EventType & EventTypeRelations;
|
||||
@@ -43,6 +43,15 @@ type Input = Pick<
|
||||
| "userId"
|
||||
| "parentId"
|
||||
| "assignAllTeamMembers"
|
||||
| "bookingLimits"
|
||||
| "durationLimits"
|
||||
| "onlyShowFirstAvailableSlot"
|
||||
| "offsetStart"
|
||||
| "periodType"
|
||||
| "periodDays"
|
||||
| "periodCountCalendarDays"
|
||||
| "periodStartDate"
|
||||
| "periodEndDate"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries-0.0.23";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsService {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
|
||||
import { Injectable, ConflictException } from "@nestjs/common";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.23";
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -5,8 +5,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.23";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.23";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.23";
|
||||
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries";
|
||||
import { ReserveSlotInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.23";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.23";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.23";
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TimezonesService {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { User } from "@calcom/prisma/client";
|
||||
@@ -21,4 +21,16 @@ export class UsersService {
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
getUserMainProfile(user: UserWithProfile) {
|
||||
return (
|
||||
user?.movedToProfile ||
|
||||
user.profiles?.find((p) => p.organizationId === user.organizationId) ||
|
||||
user.profiles?.[0]
|
||||
);
|
||||
}
|
||||
|
||||
getUserProfileByOrgId(user: UserWithProfile, organizationId: number) {
|
||||
return user.profiles?.find((p) => p.organizationId === organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { Profile, User } from "@prisma/client";
|
||||
import type { Profile, User, Team } from "@prisma/client";
|
||||
|
||||
export type UserWithProfile = User & {
|
||||
movedToProfile?: Profile | null;
|
||||
movedToProfile?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> }) | null;
|
||||
profiles?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> })[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -67,12 +68,18 @@ export class UsersRepository {
|
||||
}
|
||||
|
||||
async findByIdWithProfile(userId: number): Promise<UserWithProfile | null> {
|
||||
console.log("findByIdWithProfile");
|
||||
return this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
movedToProfile: true,
|
||||
movedToProfile: {
|
||||
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
|
||||
},
|
||||
profiles: {
|
||||
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -126,7 +133,12 @@ export class UsersRepository {
|
||||
email,
|
||||
},
|
||||
include: {
|
||||
movedToProfile: true,
|
||||
movedToProfile: {
|
||||
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
|
||||
},
|
||||
profiles: {
|
||||
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { PlatformOAuthClient, Webhook } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsOAuthClientWebhookGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { webhook: Webhook; oAuthClient: PlatformOAuthClient }>();
|
||||
const user = request.user as GetUserReturnType;
|
||||
const webhookId = request.params.webhookId;
|
||||
const oAuthClientId = request.params.clientId;
|
||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException("User not authenticated");
|
||||
}
|
||||
|
||||
if (!webhookId) {
|
||||
throw new BadRequestException("webhookId parameter not specified in the request");
|
||||
}
|
||||
|
||||
if (!webhookId) {
|
||||
throw new BadRequestException("oAuthClientId parameter not specified in the request");
|
||||
}
|
||||
|
||||
if (!user || !webhookId || !oAuthClientId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oAuthClient = await this.oAuthClientRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
if (!oAuthClient) {
|
||||
throw new NotFoundException(`OAuthClient (${oAuthClientId}) not found`);
|
||||
}
|
||||
|
||||
const webhook = await this.webhooksService.getWebhookById(webhookId);
|
||||
|
||||
if (oAuthClient?.organizationId !== organizationId) {
|
||||
return user.isSystemAdmin;
|
||||
}
|
||||
|
||||
if (webhook.platformOAuthClientId !== oAuthClientId) {
|
||||
throw new ForbiddenException("Webhook does not belong to this oAuthClient");
|
||||
}
|
||||
|
||||
request.webhook = webhook;
|
||||
request.oAuthClient = oAuthClient;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, ValidateNested } from "class-validator";
|
||||
|
||||
import { ApiResponseWithoutData } from "@calcom/platform-types";
|
||||
|
||||
import { WebhookOutputDto } from "./webhook.output";
|
||||
|
||||
export class OAuthClientWebhookOutputDto extends WebhookOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly oAuthClientId!: string;
|
||||
}
|
||||
|
||||
export class OAuthClientWebhookOutputResponseDto extends ApiResponseWithoutData {
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: OAuthClientWebhookOutputDto;
|
||||
}
|
||||
|
||||
export class OAuthClientWebhooksOutputResponseDto extends ApiResponseWithoutData {
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: OAuthClientWebhookOutputDto[];
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested, IsArray } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@@ -25,7 +25,8 @@ export class WebhookOutputDto {
|
||||
})
|
||||
readonly payloadTemplate!: string;
|
||||
|
||||
@IsEnum(WebhookTriggerEvents)
|
||||
@IsArray()
|
||||
@IsEnum(WebhookTriggerEvents, { each: true })
|
||||
@Expose()
|
||||
readonly triggers!: WebhookTriggerEvents[];
|
||||
|
||||
@@ -36,6 +37,10 @@ export class WebhookOutputDto {
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly active!: boolean;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly secret?: string;
|
||||
}
|
||||
|
||||
export class DeleteManyWebhooksOutputResponseDto {
|
||||
|
||||
@@ -4,13 +4,8 @@ import { Webhook } from "@prisma/client";
|
||||
@Injectable()
|
||||
export class WebhookOutputPipe implements PipeTransform {
|
||||
transform(value: Webhook) {
|
||||
if (value?.eventTriggers) {
|
||||
const { eventTriggers, ...rest } = value;
|
||||
const triggers = eventTriggers;
|
||||
const parsedData = { ...rest, triggers };
|
||||
return parsedData;
|
||||
}
|
||||
return value;
|
||||
const { eventTriggers, platformOAuthClientId, ...rest } = value;
|
||||
return { ...rest, triggers: eventTriggers, oAuthClientId: platformOAuthClientId };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
import { ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientWebhooksService {
|
||||
constructor(private readonly webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
async createOAuthClientWebhook(platformOAuthClientId: string, body: PipedInputWebhookType) {
|
||||
const existingWebhook = await this.webhooksRepository.getOAuthClientWebhookByUrl(
|
||||
platformOAuthClientId,
|
||||
body.subscriberUrl
|
||||
);
|
||||
if (existingWebhook) {
|
||||
throw new ConflictException("Webhook with this subscriber url already exists for this oAuth client");
|
||||
}
|
||||
|
||||
return this.webhooksRepository.createOAuthClientWebhook(platformOAuthClientId, {
|
||||
...body,
|
||||
payloadTemplate: body.payloadTemplate ?? null,
|
||||
secret: body.secret ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getOAuthClientWebhooksPaginated(platformOAuthClientId: string, skip: number, take: number) {
|
||||
return this.webhooksRepository.getOAuthClientWebhooksPaginated(platformOAuthClientId, skip, take);
|
||||
}
|
||||
|
||||
async deleteAllOAuthClientWebhooks(platformOAuthClientId: string): Promise<{ count: number }> {
|
||||
return this.webhooksRepository.deleteAllOAuthClientWebhooks(platformOAuthClientId);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,44 @@
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypeWebhooksController } from "@/modules/event-types/controllers/event-types-webhooks.controller";
|
||||
import { OAuthClientWebhooksController } from "@/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { MembershipsModule } from "../memberships/memberships.module";
|
||||
import { OrganizationsModule } from "../organizations/organizations.module";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { WebhooksController } from "./controllers/webhooks.controller";
|
||||
import { EventTypeWebhooksService } from "./services/event-type-webhooks.service";
|
||||
import { OAuthClientWebhooksService } from "./services/oauth-clients-webhooks.service";
|
||||
import { UserWebhooksService } from "./services/user-webhooks.service";
|
||||
import { WebhooksService } from "./services/webhooks.service";
|
||||
import { WebhooksRepository } from "./webhooks.repository";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, UsersModule, EventTypesModule_2024_06_14],
|
||||
controllers: [WebhooksController, EventTypeWebhooksController],
|
||||
providers: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
|
||||
exports: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
|
||||
imports: [
|
||||
PrismaModule,
|
||||
UsersModule,
|
||||
EventTypesModule_2024_06_14,
|
||||
OAuthClientModule,
|
||||
OrganizationsModule,
|
||||
MembershipsModule,
|
||||
OAuthClientModule,
|
||||
],
|
||||
controllers: [WebhooksController, EventTypeWebhooksController, OAuthClientWebhooksController],
|
||||
providers: [
|
||||
WebhooksService,
|
||||
WebhooksRepository,
|
||||
UserWebhooksService,
|
||||
EventTypeWebhooksService,
|
||||
OAuthClientWebhooksService,
|
||||
],
|
||||
exports: [
|
||||
WebhooksService,
|
||||
WebhooksRepository,
|
||||
UserWebhooksService,
|
||||
EventTypeWebhooksService,
|
||||
OAuthClientWebhooksService,
|
||||
],
|
||||
})
|
||||
export class WebhooksModule {}
|
||||
|
||||
@@ -29,6 +29,13 @@ export class WebhooksRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async createOAuthClientWebhook(platformOAuthClientId: string, data: WebhookInputData) {
|
||||
const id = uuidv4();
|
||||
return this.dbWrite.prisma.webhook.create({
|
||||
data: { ...data, id, platformOAuthClientId },
|
||||
});
|
||||
}
|
||||
|
||||
async updateWebhook(webhookId: string, data: Partial<WebhookInputData>) {
|
||||
return this.dbWrite.prisma.webhook.update({
|
||||
where: { id: webhookId },
|
||||
@@ -58,12 +65,26 @@ export class WebhooksRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getOAuthClientWebhooksPaginated(platformOAuthClientId: string, skip: number, take: number) {
|
||||
return this.dbRead.prisma.webhook.findMany({
|
||||
where: { platformOAuthClientId },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWebhookByUrl(userId: number, subscriberUrl: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { userId, subscriberUrl },
|
||||
});
|
||||
}
|
||||
|
||||
async getOAuthClientWebhookByUrl(platformOAuthClientId: string, subscriberUrl: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { platformOAuthClientId, subscriberUrl },
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeWebhookByUrl(eventTypeId: number, subscriberUrl: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { eventTypeId, subscriberUrl },
|
||||
@@ -81,4 +102,10 @@ export class WebhooksRepository {
|
||||
where: { eventTypeId },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAllOAuthClientWebhooks(oAuthClientId: string) {
|
||||
return this.dbWrite.prisma.webhook.deleteMany({
|
||||
where: { platformOAuthClientId: oAuthClientId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3594,6 +3594,238 @@
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth-clients/{clientId}/webhooks": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientWebhooksController_createOAuthClientWebhook",
|
||||
"summary": "Create a webhook for an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "OAuthClientWebhooksController_getOAuthClientWebhooks",
|
||||
"summary": "Get all webhooks of an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OAuthClientWebhooksController_deleteAllOAuthClientWebhooks",
|
||||
"summary": "Delete all webhooks of an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth-clients/{clientId}/webhooks/{webhookId}": {
|
||||
"patch": {
|
||||
"operationId": "OAuthClientWebhooksController_updateOAuthClientWebhook",
|
||||
"summary": "Update a webhook of an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "OAuthClientWebhooksController_getOAuthClientWebhook",
|
||||
"summary": "Get a webhook of an oAuthClient",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OAuthClientWebhooksController_deleteOAuthClientWebhook",
|
||||
"summary": "Delete a webhook of an oAuthClient",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/destination-calendars": {
|
||||
"put": {
|
||||
"operationId": "DestinationCalendarsController_updateDestinationCalendars",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DestinationCalendarsInputBodyDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DestinationCalendarsOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Destination-Calendars",
|
||||
"Select a third party destination calendar where events will be created"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -6399,33 +6631,6 @@
|
||||
"type": "string",
|
||||
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"bookingFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"disableGuests": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"slotInterval": {
|
||||
"type": "number"
|
||||
},
|
||||
"minimumBookingNotice": {
|
||||
"type": "number"
|
||||
},
|
||||
"beforeEventBuffer": {
|
||||
"type": "number"
|
||||
},
|
||||
"afterEventBuffer": {
|
||||
"type": "number"
|
||||
},
|
||||
"schedulingType": {
|
||||
"type": "object"
|
||||
},
|
||||
@@ -6443,13 +6648,6 @@
|
||||
"lengthInMinutes",
|
||||
"title",
|
||||
"description",
|
||||
"locations",
|
||||
"bookingFields",
|
||||
"disableGuests",
|
||||
"slotInterval",
|
||||
"minimumBookingNotice",
|
||||
"beforeEventBuffer",
|
||||
"afterEventBuffer",
|
||||
"schedulingType",
|
||||
"hosts",
|
||||
"assignAllTeamMembers"
|
||||
@@ -6547,6 +6745,64 @@
|
||||
"userId"
|
||||
]
|
||||
},
|
||||
"BookingLimitsCount_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"day": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"description": "The number of bookings per day",
|
||||
"example": 1
|
||||
},
|
||||
"week": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"description": "The number of bookings per week",
|
||||
"example": 2
|
||||
},
|
||||
"month": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"description": "The number of bookings per month",
|
||||
"example": 3
|
||||
},
|
||||
"year": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"description": "The number of bookings per year",
|
||||
"example": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"BookingLimitsDuration_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"day": {
|
||||
"type": "number",
|
||||
"minimum": 15,
|
||||
"description": "The duration of bookings per day (must be a multiple of 15)",
|
||||
"example": 60
|
||||
},
|
||||
"week": {
|
||||
"type": "number",
|
||||
"minimum": 15,
|
||||
"description": "The duration of bookings per week (must be a multiple of 15)",
|
||||
"example": 120
|
||||
},
|
||||
"month": {
|
||||
"type": "number",
|
||||
"minimum": 15,
|
||||
"description": "The duration of bookings per month (must be a multiple of 15)",
|
||||
"example": 180
|
||||
},
|
||||
"year": {
|
||||
"type": "number",
|
||||
"minimum": 15,
|
||||
"description": "The duration of bookings per year (must be a multiple of 15)",
|
||||
"example": 240
|
||||
}
|
||||
}
|
||||
},
|
||||
"TeamEventTypeOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6666,6 +6922,22 @@
|
||||
},
|
||||
"assignAllTeamMembers": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"bookingLimitsCount": {
|
||||
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
|
||||
},
|
||||
"onlyShowFirstAvailableSlot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"bookingLimitsDuration": {
|
||||
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
|
||||
},
|
||||
"bookingWindow": {
|
||||
"type": "object"
|
||||
},
|
||||
"offsetStart": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7525,6 +7797,21 @@
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"MeOrgOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isPlatform": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isPlatform",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"MeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7553,6 +7840,9 @@
|
||||
"organizationId": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"organization": {
|
||||
"$ref": "#/components/schemas/MeOrgOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8544,9 +8834,6 @@
|
||||
"noShowHost": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"messageKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -8660,6 +8947,9 @@
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8787,6 +9077,9 @@
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8860,6 +9153,156 @@
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"OAuthClientWebhookOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"oAuthClientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"payloadTemplate",
|
||||
"oAuthClientId",
|
||||
"id",
|
||||
"triggers",
|
||||
"subscriberUrl",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"OAuthClientWebhookOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"OAuthClientWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"DestinationCalendarsInputBodyDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"integration": {
|
||||
"type": "string",
|
||||
"example": "apple_calendar",
|
||||
"description": "The calendar service you want to integrate, as returned by the /calendars endpoint",
|
||||
"enum": [
|
||||
"apple_calendar",
|
||||
"google_calendar",
|
||||
"office365_calendar"
|
||||
]
|
||||
},
|
||||
"externalId": {
|
||||
"type": "string",
|
||||
"example": "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
|
||||
"description": "Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"integration",
|
||||
"externalId"
|
||||
]
|
||||
},
|
||||
"DestinationCalendarsOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "number"
|
||||
},
|
||||
"integration": {
|
||||
"type": "string"
|
||||
},
|
||||
"externalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"credentialId": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"userId",
|
||||
"integration",
|
||||
"externalId",
|
||||
"credentialId"
|
||||
]
|
||||
},
|
||||
"DestinationCalendarsOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/DestinationCalendarsOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,15 @@ export class EventTypesRepositoryFixture {
|
||||
});
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Pick<CreateEventTypeInput_2024_04_15, "title" | "slug" | "length" | "hidden">,
|
||||
userId: number
|
||||
) {
|
||||
async create(data: Prisma.EventTypeCreateInput, userId: number) {
|
||||
return this.prismaWriteClient.eventType.create({
|
||||
data: {
|
||||
...data,
|
||||
userId,
|
||||
owner: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -5,6 +5,7 @@ import { headers, cookies } from "next/headers";
|
||||
import React from "react";
|
||||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { IconSprites } from "@calcom/ui";
|
||||
|
||||
import { prepareRootMetadata } from "@lib/metadata";
|
||||
|
||||
@@ -83,6 +84,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
--font-cal: ${calFont.style.fontFamily.replace(/\'/g, "")};
|
||||
}
|
||||
`}</style>
|
||||
<IconSprites />
|
||||
</head>
|
||||
<body
|
||||
className="dark:bg-darkgray-50 bg-subtle antialiased"
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
|
||||
import { AppListCard as AppListCardComponent } from "@calcom/ui";
|
||||
|
||||
type ShouldHighlight =
|
||||
| {
|
||||
@@ -21,7 +20,7 @@ type ShouldHighlight =
|
||||
slug?: never;
|
||||
};
|
||||
|
||||
type AppListCardProps = {
|
||||
export type AppListCardProps = {
|
||||
logo?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -37,21 +36,7 @@ type AppListCardProps = {
|
||||
const schema = z.object({ hl: z.string().optional() });
|
||||
|
||||
export default function AppListCard(props: AppListCardProps) {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
isDefault,
|
||||
slug,
|
||||
shouldHighlight,
|
||||
isTemplate,
|
||||
invalidCredential,
|
||||
children,
|
||||
credentialOwner,
|
||||
className,
|
||||
} = props;
|
||||
const { slug, shouldHighlight } = props;
|
||||
const {
|
||||
data: { hl },
|
||||
} = useTypedQuery(schema);
|
||||
@@ -83,53 +68,5 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
};
|
||||
}, [highlight, pathname, router, searchParams, shouldHighlight]);
|
||||
|
||||
return (
|
||||
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100", className)}>
|
||||
<div className="flex items-center gap-x-3 px-4 py-4 sm:px-6">
|
||||
{logo ? (
|
||||
<img
|
||||
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
|
||||
src={logo}
|
||||
alt={`${title} logo`}
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex grow flex-col gap-y-1 truncate">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
||||
{isTemplate && <Badge variant="red">Template</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
<ListItemText component="p">{description}</ListItemText>
|
||||
{invalidCredential && (
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{credentialOwner && (
|
||||
<div>
|
||||
<Badge variant="gray">
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-2"
|
||||
alt={credentialOwner.name || "Nameless"}
|
||||
size="xs"
|
||||
imageSrc={getPlaceholderAvatar(credentialOwner.avatar, credentialOwner?.name as string)}
|
||||
/>
|
||||
{credentialOwner.name}
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actions}
|
||||
</div>
|
||||
{children && <div className="w-full">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
return <AppListCardComponent {...props} />;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function EnterprisePage() {
|
||||
description: t("branded_subdomain_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="bar-chart" className="h-5 w-5 text-blue-500" />,
|
||||
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
|
||||
title: t("org_insights"),
|
||||
description: t("org_insights_description"),
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user