Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97b2816897 | ||
|
|
00372b6b3b | ||
|
|
6c8fced247 | ||
|
|
52813b01e6 | ||
|
|
77a98d96c8 | ||
|
|
676eac357f | ||
|
|
5bafd1b264 | ||
|
|
0391bfe887 | ||
|
|
c18407d75a | ||
|
|
a631286770 | ||
|
|
dd78741158 | ||
|
|
8f69811e25 | ||
|
|
3a63a8b228 | ||
|
|
ef98105503 | ||
|
|
a18f1eb382 | ||
|
|
2ab6315a8e | ||
|
|
258ebe0420 | ||
|
|
1cfd2f7053 | ||
|
|
a51024e915 | ||
|
|
900e90cd2c | ||
|
|
46ffeaee82 | ||
|
|
22425bb693 | ||
|
|
ad0b8a668c | ||
|
|
bbad2c9d31 | ||
|
|
8b28a6f5df | ||
|
|
93bb37702c | ||
|
|
4a8f251d89 | ||
|
|
cf44919cdd | ||
|
|
024e40b26b | ||
|
|
7c354321be | ||
|
|
fc16ec59d3 | ||
|
|
ab26481738 | ||
|
|
2fbc06314e | ||
|
|
386006fad0 | ||
|
|
ceb555460e | ||
|
|
c28870e029 | ||
|
|
0650898302 | ||
|
|
d004815a68 | ||
|
|
5d76bd6b9c | ||
|
|
d02947f887 | ||
|
|
a3a95a1496 | ||
|
|
d47c6b3fdb | ||
|
|
ebbb1dcce3 | ||
|
|
8fbb1ed426 | ||
|
|
d8adc8eeb1 | ||
|
|
a7bd4aee85 | ||
|
|
82fcda4b42 | ||
|
|
b3b7a09cfb | ||
|
|
60ae817f30 | ||
|
|
74166fc2cb | ||
|
|
fb997045f2 | ||
|
|
73a79cf29b | ||
|
|
54a1663a8b | ||
|
|
883d745784 | ||
|
|
571e93a19a | ||
|
|
d16428da6c | ||
|
|
8c5438663c | ||
|
|
ed76d163cf | ||
|
|
9215725dad | ||
|
|
00131668aa | ||
|
|
0306ad737f | ||
|
|
20dfc34e70 | ||
|
|
b7e8b252d2 | ||
|
|
74f09c332b | ||
|
|
3ce6c23603 | ||
|
|
ad3ee14501 | ||
|
|
1931758fc7 | ||
|
|
fe551af145 | ||
|
|
9fdaaed100 | ||
|
|
c0f8a355e3 | ||
|
|
df95ec4ce8 | ||
|
|
a15cad4d03 | ||
|
|
8904f20eb8 | ||
|
|
0ee81d0739 | ||
|
|
c0bc6f8ccb | ||
|
|
1f1cd1e236 | ||
|
|
dbbd78fb26 | ||
|
|
f6e45842fd | ||
|
|
d25c043046 | ||
|
|
5695ba7c13 | ||
|
|
eeb0322ce1 | ||
|
|
480977c2b5 | ||
|
|
8b2e46a6fa | ||
|
|
842be660b0 | ||
|
|
4f69c0e502 | ||
|
|
be128181e2 | ||
|
|
0aa1628925 | ||
|
|
fd58f669d0 | ||
|
|
51927f82c0 | ||
|
|
0f8a0eaf4a | ||
|
|
b9ac22d4ed | ||
|
|
ebca5c6409 | ||
|
|
6ef35e4dba | ||
|
|
8bb5200dd8 | ||
|
|
4aeb6b7c9b | ||
|
|
ae64d7ba82 | ||
|
|
9dd2dbff19 | ||
|
|
cebbffe230 | ||
|
|
e3e464abfa | ||
|
|
268438b0ff | ||
|
|
355c8de5e7 | ||
|
|
e6056bc4a4 | ||
|
|
f2eebe95bd | ||
|
|
3084013269 | ||
|
|
68568e5a25 | ||
|
|
0eae0bb8fe | ||
|
|
b61820d354 | ||
|
|
1606272850 | ||
|
|
ea04fe1c9b | ||
|
|
4c50b6a8af | ||
|
|
39aa848e9e | ||
|
|
64c56c1b17 | ||
|
|
8e883a6b10 | ||
|
|
9684aa3ad5 | ||
|
|
5fede84bf6 | ||
|
|
a788e07be1 | ||
|
|
68ffd2248b | ||
|
|
4b12d021e4 | ||
|
|
4daf9db9db | ||
|
|
a660b1eb05 | ||
|
|
6f2f098e12 | ||
|
|
6a75ef86b0 | ||
|
|
3d2397879c | ||
|
|
de474089a7 | ||
|
|
8b31b9aa05 | ||
|
|
ad41672958 | ||
|
|
86f2984b30 | ||
|
|
38e62786e4 | ||
|
|
ac3d7c9fae | ||
|
|
98f94ea694 | ||
|
|
8ad80f7ac3 | ||
|
|
296678046c | ||
|
|
fd193e651d | ||
|
|
becad5ee67 | ||
|
|
161dcbdd4c | ||
|
|
7ad6652cb0 | ||
|
|
c467167e91 | ||
|
|
18093824d8 | ||
|
|
8102f3c33c | ||
|
|
902c441248 | ||
|
|
e641bb3d57 | ||
|
|
19d938ed60 | ||
|
|
5561949f9b | ||
|
|
5e98cca062 | ||
|
|
ac858c9bee | ||
|
|
f58f5e4e02 | ||
|
|
ad844f5e51 | ||
|
|
53c1bc45d6 | ||
|
|
0cc93aa87e | ||
|
|
b41b830086 | ||
|
|
c79d0890c0 | ||
|
|
5071e6566f | ||
|
|
ac77fe360d | ||
|
|
711aceba0e | ||
|
|
f010279303 | ||
|
|
0c67f4940a | ||
|
|
7836aeacc6 | ||
|
|
af0cc5c899 | ||
|
|
aa2808d621 | ||
|
|
621067acd8 | ||
|
|
3fcba33b1b | ||
|
|
9a66dae0ba | ||
|
|
e2ef8d2dc8 | ||
|
|
eb089bb207 | ||
|
|
4565c3311c | ||
|
|
e30b41a85a | ||
|
|
7b0321043d | ||
|
|
c4b7062d65 | ||
|
|
eb1c1b9475 | ||
|
|
67725f0cba | ||
|
|
68c4ad2bf9 | ||
|
|
dbafda9822 | ||
|
|
6d5d2a1c4d | ||
|
|
b748b8d077 | ||
|
|
f86c448359 | ||
|
|
d0af4b8ddd | ||
|
|
39103c07c9 | ||
|
|
8382ebc057 | ||
|
|
880ba8f1dd | ||
|
|
f00849b8d1 | ||
|
|
c090f78898 | ||
|
|
46a1713122 | ||
|
|
4a88b33782 | ||
|
|
a1a2a141e4 | ||
|
|
97404455d4 | ||
|
|
be31100bd7 | ||
|
|
4335c9ab92 | ||
|
|
20df7156cf | ||
|
|
9c35425e57 | ||
|
|
65df5ac0e4 | ||
|
|
84fdd03603 | ||
|
|
e6ec8bdf4d | ||
|
|
86c8197104 | ||
|
|
bb13badcf4 | ||
|
|
64b2397fa2 | ||
|
|
90130457e2 | ||
|
|
260521fb28 | ||
|
|
6390d595f5 | ||
|
|
74dc46f8b2 | ||
|
|
67262910cd | ||
|
|
d59e68de6d | ||
|
|
b50318fbca | ||
|
|
1ce7df324d | ||
|
|
e3b7264208 | ||
|
|
5e3f6e6c2c | ||
|
|
5b6c43118e | ||
|
|
beee545568 | ||
|
|
a597defd03 | ||
|
|
e3562cdfb5 | ||
|
|
86a7824ec9 | ||
|
|
7001515a44 | ||
|
|
36044c4ab3 | ||
|
|
4933295810 | ||
|
|
ed0cd30092 | ||
|
|
1132d9d0dc | ||
|
|
9bd3a362a2 | ||
|
|
844076badb | ||
|
|
c0523a31a7 | ||
|
|
1fff642860 | ||
|
|
89fe383c9f | ||
|
|
4a173c48b8 | ||
|
|
a72a3e45e9 | ||
|
|
a8d8c992a4 | ||
|
|
99f9a121cb | ||
|
|
6a0e2392fe | ||
|
|
addec46feb | ||
|
|
a4fe9a51b5 | ||
|
|
78b8bbb5aa | ||
|
|
d7ece4f583 | ||
|
|
8fc9b9ae9d | ||
|
|
d1175bf7de | ||
|
|
3900453482 | ||
|
|
68e7e76b31 | ||
|
|
986d196a37 | ||
|
|
d30fb213d2 | ||
|
|
3f02de8214 | ||
|
|
7421cea0da | ||
|
|
507daa6a0c | ||
|
|
61f68af435 | ||
|
|
b3f4cfab32 | ||
|
|
c3b1de498d | ||
|
|
cdf059361e | ||
|
|
2aada9a7da |
@@ -38,6 +38,7 @@ BASECAMP3_USER_AGENT=
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
DAILY_WEBHOOK_SECRET=''
|
||||
DAILY_MEETING_ENDED_WEBHOOK_SECRET=''
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
|
||||
+11
-3
@@ -169,8 +169,7 @@ STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
PAYMENT_FEE_FIXED=
|
||||
PAYMENT_FEE_PERCENTAGE=
|
||||
|
||||
|
||||
# Use for internal Public API Keys and optional
|
||||
API_KEY_PREFIX=cal_
|
||||
@@ -213,6 +212,10 @@ E2E_TEST_MAILHOG_ENABLED=
|
||||
|
||||
# **********************************************************************************************************
|
||||
|
||||
# Cloudflare Turnstile
|
||||
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
|
||||
CLOUDFLARE_TURNSTILE_SECRET=
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||
|
||||
@@ -237,7 +240,7 @@ CSP_POLICY=
|
||||
# Vercel Edge Config
|
||||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
|
||||
|
||||
# Control time intervals on a user's Schedule availability
|
||||
@@ -296,6 +299,7 @@ E2E_TEST_CALCOM_GCAL_KEYS=
|
||||
CALCOM_CREDENTIAL_SYNC_SECRET=""
|
||||
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
|
||||
CALCOM_CREDENTIAL_SYNC_HEADER_NAME="calcom-credential-sync-secret"
|
||||
# This the endpoint from which the token is fetched
|
||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||
# Key should match on Cal.com and your application
|
||||
# must be 24 bytes for AES256 encryption algorithm
|
||||
@@ -344,6 +348,10 @@ SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
# api v2
|
||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||
|
||||
# Tasker features
|
||||
TASKER_ENABLE_WEBHOOKS=0
|
||||
TASKER_ENABLE_EMAILS=0
|
||||
|
||||
# Ratelimiting via unkey
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ node_modules
|
||||
**/**/public
|
||||
packages/prisma/zod
|
||||
apps/web/public/embed
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Production Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build API V1
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
shell: bash
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Cron - changeTimeZone
|
||||
|
||||
on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs "At every full hour." (see https://crontab.guru)
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
cron-scheduleEmailReminders:
|
||||
env:
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||
run: |
|
||||
curl ${{ secrets.APP_URL }}/api/cron/changeTimeZone \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
-sSf
|
||||
@@ -62,6 +62,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -91,6 +92,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -49,6 +49,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -73,6 +74,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -55,6 +55,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -80,6 +81,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
shard: [1, 2, 3, 4, 5, 6, 7]
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -53,6 +53,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -82,6 +83,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -56,6 +56,13 @@ jobs:
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v1:
|
||||
name: Production build
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
needs: [changes]
|
||||
@@ -99,7 +106,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [changes, lint, type-check, test, build, build-api-v1, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
@@ -31,6 +32,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
@@ -35,6 +36,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -17,3 +17,4 @@ packages/prisma/zod
|
||||
packages/prisma/enums
|
||||
apps/web/public/embed
|
||||
apps/api/v2/swagger/documentation.json
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
+13
-2
@@ -13,8 +13,6 @@ Contributions are what makes the open source community such an amazing place to
|
||||
- Issues from non-core members automatically receive the `🚨 needs approval` label.
|
||||
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
|
||||
|
||||
|
||||
|
||||
## Priorities
|
||||
|
||||
<table>
|
||||
@@ -178,11 +176,15 @@ If you get errors, be sure to fix them before committing.
|
||||
Do not commit your `yarn.lock` unless you've made changes to the `package.json`. If you've already committed `yarn.lock` unintentionally, follow these steps to undo:
|
||||
|
||||
If your last commit has the `yarn.lock` file alongside other files and you only wish to uncommit the `yarn.lock`:
|
||||
|
||||
```bash
|
||||
git checkout HEAD~1 yarn.lock
|
||||
git commit -m "Revert yarn.lock changes"
|
||||
```
|
||||
|
||||
_NB_: You may have to bypass the pre-commit hook with by appending `--no-verify` to the git commit
|
||||
If you've pushed the commit with the `yarn.lock`:
|
||||
|
||||
1. Correct the commit locally using the above method.
|
||||
2. Carefully force push:
|
||||
|
||||
@@ -194,26 +196,35 @@ If `yarn.lock` was committed a while ago and there have been several commits sin
|
||||
|
||||
1. **Checkout a Previous Version**:
|
||||
- Find the commit hash before the `yarn.lock` was unintentionally committed. You can do this by viewing the Git log:
|
||||
|
||||
```bash
|
||||
git log yarn.lock
|
||||
```
|
||||
|
||||
- Once you have identified the commit hash, use it to checkout the previous version of `yarn.lock`:
|
||||
|
||||
```bash
|
||||
git checkout <commit_hash> yarn.lock
|
||||
```
|
||||
|
||||
2. **Commit the Reverted Version**:
|
||||
- After checking out the previous version of the `yarn.lock`, commit this change:
|
||||
|
||||
```bash
|
||||
git commit -m "Revert yarn.lock to its state before unintended changes"
|
||||
```
|
||||
|
||||
3. **Proceed with Caution**:
|
||||
- If you need to push this change, first pull the latest changes from your remote branch to ensure you're not overwriting other recent changes:
|
||||
|
||||
```bash
|
||||
git pull origin <your-branch-name>
|
||||
```
|
||||
|
||||
- Then push the updated branch:
|
||||
|
||||
```bash
|
||||
git push origin <your-branch-name>
|
||||
```
|
||||
|
||||
Lastly, make sure to keep the branches updated (e.g. click the `Update branch` button on GitHub PR).
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://go.cal.com/discord"><img src="https://img.shields.io/badge/Discord-go.cal.com%2Fdiscord-%234A154B" alt="Join Cal.com Discord"></a>
|
||||
<a href="https://producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||
<a href="https://www.producthunt.com/products/cal-com"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
||||
@@ -33,7 +33,7 @@
|
||||
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
|
||||
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
||||
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
|
||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||
<a href="https://hub.docker.com/r/calcom/cal.com"><img src="https://img.shields.io/docker/pulls/calcom/cal.com"></a>
|
||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||
@@ -113,7 +113,7 @@ To get a local copy up and running, please follow these simple steps.
|
||||
Here is what you need to be able to run Cal.com.
|
||||
|
||||
- Node.js (Version: >=18.x)
|
||||
- PostgreSQL
|
||||
- PostgreSQL (Version: >=13.x)
|
||||
- Yarn _(recommended)_
|
||||
|
||||
> If you want to enable any of the available integrations, you may want to obtain additional credentials for each one. More details on this can be found below under the [integrations section](#integrations).
|
||||
@@ -210,7 +210,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||

|
||||
|
||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`. The port is configurable and does not have to be 5432.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
|
||||
try {
|
||||
return get<T>(key);
|
||||
} catch (error) {
|
||||
// Don't crash if EDGE_CONFIG env var is missing
|
||||
}
|
||||
};
|
||||
|
||||
export const config = { matcher: "/:path*" };
|
||||
|
||||
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
|
||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
||||
if (isInMaintenanceMode) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "API is currently under maintenance. Please try again at a later time." });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit";
|
||||
|
||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
@@ -11,7 +10,7 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
identifier: req.query.apiKey as string,
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: (response) => {
|
||||
res.setHeader("X-RateLimit-Limit", API_KEY_RATE_LIMIT);
|
||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { label } from "next-api-middleware";
|
||||
|
||||
import { addRequestId } from "./addRequestid";
|
||||
import { captureErrors } from "./captureErrors";
|
||||
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
|
||||
import { extendRequest } from "./extendRequest";
|
||||
import {
|
||||
HTTP_POST,
|
||||
@@ -24,6 +25,7 @@ const middleware = {
|
||||
HTTP_POST,
|
||||
HTTP_DELETE,
|
||||
addRequestId,
|
||||
checkIsInMaintenanceMode,
|
||||
verifyApiKey,
|
||||
rateLimitApiKey,
|
||||
extendRequest,
|
||||
@@ -36,6 +38,7 @@ type Middleware = keyof typeof middleware;
|
||||
|
||||
const middlewareOrder = [
|
||||
// The order here, determines the order of execution
|
||||
"checkIsInMaintenanceMode",
|
||||
"extendRequest",
|
||||
"captureErrors",
|
||||
"verifyApiKey",
|
||||
|
||||
@@ -6,6 +6,9 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
|
||||
hideBranding: true,
|
||||
metadata: true,
|
||||
pendingPayment: true,
|
||||
isOrganization: true,
|
||||
isPlatform: true,
|
||||
smsLockState: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({
|
||||
|
||||
@@ -82,7 +82,6 @@ export const schemaUserBaseBodyParams = User.pick({
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
away: true,
|
||||
role: true,
|
||||
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
|
||||
// avatar: true,
|
||||
@@ -171,7 +170,6 @@ export const schemaUserReadPublic = User.pick({
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
away: true,
|
||||
createdDate: true,
|
||||
verified: true,
|
||||
invitedTo: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { minimumTokenResponseSchema } from "@calcom/app-store/_utils/oauth/parseRefreshTokenResponse";
|
||||
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -63,7 +63,7 @@ async function handler(req: NextApiRequest) {
|
||||
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
||||
);
|
||||
|
||||
const key = minimumTokenResponseSchema.parse(decryptedKey);
|
||||
const key = OAuth2UniversalSchema.parse(decryptedKey);
|
||||
|
||||
const credential = await prisma.credential.update({
|
||||
where: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { minimumTokenResponseSchema } from "@calcom/app-store/_utils/oauth/parseRefreshTokenResponse";
|
||||
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -70,7 +70,7 @@ async function handler(req: NextApiRequest) {
|
||||
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
||||
);
|
||||
|
||||
const key = minimumTokenResponseSchema.parse(decryptedKey);
|
||||
const key = OAuth2UniversalSchema.parse(decryptedKey);
|
||||
|
||||
// Need to get app type
|
||||
const app = await prisma.app.findUnique({
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: { id, users: { some: { id: userId } } },
|
||||
});
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
@@ -93,6 +93,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (eventType?.userId === req.userId) return true; // is owner.
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -4,11 +4,8 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
|
||||
@@ -26,7 +26,7 @@ export default async function checkTeamEventEditPermission(
|
||||
|
||||
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
statusCode: 403,
|
||||
message: "No permission to operate on event-type for this team",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { purchaseTeamSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
@@ -105,10 +105,11 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
};
|
||||
delete data.slug;
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
const checkoutSession = await purchaseTeamSubscription({
|
||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||
teamId: _team.id,
|
||||
seats: _team.members.length,
|
||||
userId,
|
||||
pricePerSeat: null,
|
||||
});
|
||||
if (!checkoutSession.url)
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
@@ -197,8 +198,9 @@ const generateTeamCheckoutSession = async ({
|
||||
customer_update: {
|
||||
address: "auto",
|
||||
},
|
||||
// Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
|
||||
automatic_tax: {
|
||||
enabled: true,
|
||||
enabled: IS_PRODUCTION,
|
||||
},
|
||||
metadata: {
|
||||
pendingPaymentTeamId,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { buildEventType } from "@calcom/lib/test/builder";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import handler from "../../../../pages/api/event-types/[id]/_get";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("GET /api/event-types/[id]", () => {
|
||||
describe("Errors", () => {
|
||||
test("Returns 403 if user not admin/team member/event owner", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
id: 123456,
|
||||
},
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({
|
||||
id: 123456,
|
||||
userId: 444444,
|
||||
})
|
||||
);
|
||||
|
||||
req.userId = 333333;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", async () => {
|
||||
test("Returns event type if user is admin", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({
|
||||
id: eventTypeId,
|
||||
})
|
||||
);
|
||||
|
||||
req.isAdmin = true;
|
||||
req.userId = 333333;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
});
|
||||
|
||||
test("Returns event type if user is in team associated with event type", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const teamId = 9999;
|
||||
const userId = 333333;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({
|
||||
id: eventTypeId,
|
||||
teamId,
|
||||
})
|
||||
);
|
||||
|
||||
prismaMock.team.findFirst.mockResolvedValue({
|
||||
id: teamId,
|
||||
members: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
req.isAdmin = false;
|
||||
req.userId = userId;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
expect(prismaMock.team.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId: req.userId,
|
||||
role: {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Returns event type if user is the event type owner", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const userId = 333333;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({
|
||||
id: eventTypeId,
|
||||
userId,
|
||||
scheduleId: 1111,
|
||||
})
|
||||
);
|
||||
|
||||
req.isAdmin = false;
|
||||
req.userId = userId;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
expect(prismaMock.team.findFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,8 @@
|
||||
"paths": {
|
||||
"~/*": ["*"],
|
||||
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||
}
|
||||
},
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
@@ -10,4 +10,13 @@ JWT_SECRET=
|
||||
SENTRY_DSN=
|
||||
|
||||
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Stripe Billing
|
||||
STRIPE_PRICE_ID_STARTER=
|
||||
STRIPE_PRICE_ID_ESSENTIALS=
|
||||
STRIPE_PRICE_ID_ENTERPRISE=
|
||||
STRIPE_API_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
WEB_APP_URL=http://localhost:3000/
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js"],
|
||||
ignorePatterns: [".eslintrc.js", "next-i18next.config.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "./apps/api/v2/src/main.js",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"plugins": [
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"start": "nest start",
|
||||
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch & yarn workspace @calcom/platform-libraries build:watch",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
|
||||
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && nest start --watch",
|
||||
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/src/main",
|
||||
"start:prod": "SKIP_DOCS_GENERATION=1 node ./dist/apps/api/v2/src/main.js",
|
||||
"test": "yarn dev:build && jest",
|
||||
"test:watch": "yarn dev:build && jest --watch",
|
||||
"test:cov": "yarn dev:build && jest --coverage",
|
||||
@@ -37,9 +37,10 @@
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "^5.1.1",
|
||||
"@nestjs/throttler": "^5.1.2",
|
||||
"@sentry/node": "^7.86.0",
|
||||
"@sentry/tracing": "^7.86.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
@@ -56,6 +57,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"stripe": "^15.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.22.4"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
@Controller()
|
||||
@DocsTags("Health - development only")
|
||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||
export class AppController {
|
||||
@Get("health")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { AppLoggerMiddleware } from "@/app.logger.middleware";
|
||||
import appConfig from "@/config/app";
|
||||
import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware";
|
||||
import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware";
|
||||
import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware";
|
||||
import { RawBodyMiddleware } from "@/middleware/body/raw.body.middleware";
|
||||
import { ResponseInterceptor } from "@/middleware/request-ids/request-id.interceptor";
|
||||
import { RequestIdMiddleware } from "@/middleware/request-ids/request-id.middleware";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { EndpointsModule } from "@/modules/endpoints.module";
|
||||
import { JwtModule } from "@/modules/jwt/jwt.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { APP_INTERCEPTOR, RouterModule } from "@nestjs/core";
|
||||
import { seconds, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
|
||||
|
||||
import { AppController } from "./app.controller";
|
||||
|
||||
@@ -16,29 +26,56 @@ import { AppController } from "./app.controller";
|
||||
isGlobal: true,
|
||||
load: [appConfig],
|
||||
}),
|
||||
// ThrottlerModule.forRootAsync({
|
||||
// imports: [ConfigModule],
|
||||
// inject: [ConfigService],
|
||||
// useFactory: (config: ConfigService<AppConfig>) => ({
|
||||
// throttlers: [
|
||||
// {
|
||||
// name: "short",
|
||||
// ttl: seconds(10),
|
||||
// limit: 3,
|
||||
// },
|
||||
// ],
|
||||
// storage: new ThrottlerStorageRedisService(config.get("db.redisUrl", { infer: true })),
|
||||
// }),
|
||||
// }),
|
||||
RedisModule,
|
||||
ThrottlerModule.forRootAsync({
|
||||
imports: [RedisModule],
|
||||
inject: [RedisService],
|
||||
useFactory: (redisService: RedisService) => ({
|
||||
throttlers: [
|
||||
{
|
||||
name: "short",
|
||||
ttl: seconds(10),
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
name: "medium",
|
||||
ttl: seconds(30),
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
storage: new ThrottlerStorageRedisService(redisService.redis),
|
||||
}),
|
||||
}),
|
||||
PrismaModule,
|
||||
EndpointsModule,
|
||||
AuthModule,
|
||||
JwtModule,
|
||||
//register prefix for all routes in EndpointsModule
|
||||
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ResponseInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer): void {
|
||||
consumer.apply(AppLoggerMiddleware).forRoutes("*");
|
||||
consumer
|
||||
.apply(RawBodyMiddleware)
|
||||
.forRoutes({
|
||||
path: "/api/v2/billing/webhook",
|
||||
method: RequestMethod.POST,
|
||||
})
|
||||
.apply(JsonBodyMiddleware)
|
||||
.forRoutes("*")
|
||||
.apply(RequestIdMiddleware)
|
||||
.forRoutes("*")
|
||||
.apply(AppLoggerMiddleware)
|
||||
.forRoutes("*")
|
||||
.apply(RewriterMiddleware)
|
||||
.forRoutes("/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { SentryFilter } from "@/filters/sentry-exception.filter";
|
||||
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
|
||||
import type { ValidationError } from "@nestjs/common";
|
||||
import { BadRequestException, RequestMethod, ValidationPipe, VersioningType } from "@nestjs/common";
|
||||
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
|
||||
import { HttpAdapterHost } from "@nestjs/core";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import helmet from "helmet";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
|
||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||
@@ -26,7 +28,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.enableCors({
|
||||
origin: "*",
|
||||
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
||||
allowedHeaders: ["Accept", "Authorization", "Content-Type", "Origin"],
|
||||
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
|
||||
maxAge: 86_400,
|
||||
});
|
||||
|
||||
@@ -58,10 +60,6 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalFilters(new TRPCExceptionFilter());
|
||||
|
||||
app.setGlobalPrefix("api", {
|
||||
exclude: [{ path: "health", method: RequestMethod.GET }],
|
||||
});
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
return app;
|
||||
|
||||
@@ -11,8 +11,10 @@ const loadConfig = (): AppConfig => {
|
||||
port: Number(getEnv("API_PORT", "5555")),
|
||||
path: getEnv("API_URL", "http://localhost"),
|
||||
url: `${getEnv("API_URL", "http://localhost")}${
|
||||
process.env.API_PORT ? `:${Number(getEnv("API_PORT", "5555"))}` : ""
|
||||
}/api/v2`,
|
||||
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
|
||||
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||
: ""
|
||||
}/v2`,
|
||||
},
|
||||
db: {
|
||||
readUrl: getEnv("DATABASE_READ_URL"),
|
||||
@@ -22,6 +24,13 @@ const loadConfig = (): AppConfig => {
|
||||
next: {
|
||||
authSecret: getEnv("NEXTAUTH_SECRET"),
|
||||
},
|
||||
stripe: {
|
||||
apiKey: getEnv("STRIPE_API_KEY"),
|
||||
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
|
||||
},
|
||||
app: {
|
||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -15,4 +15,11 @@ export type AppConfig = {
|
||||
next: {
|
||||
authSecret: string;
|
||||
};
|
||||
stripe: {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
app: {
|
||||
baseUrl: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TokensModule],
|
||||
providers: [TokensRepository, OAuthFlowService],
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
|
||||
controllers: [BookingsController],
|
||||
})
|
||||
export class BookingsModule {}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -11,17 +15,12 @@ import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
handleNewBooking,
|
||||
getBookingInfo,
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Bookings Endpoints", () => {
|
||||
@@ -30,28 +29,50 @@ describe("Bookings Endpoints", () => {
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let eventTypeId: number;
|
||||
|
||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
|
||||
providers: [SchedulesRepository, SchedulesService],
|
||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, SchedulesModule],
|
||||
})
|
||||
).compile();
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService>(SchedulesService);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60 },
|
||||
user.id
|
||||
);
|
||||
eventTypeId = event.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
@@ -64,13 +85,23 @@ describe("Bookings Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingStart = "2040-05-21T09:30:00.000Z";
|
||||
const bookingEnd = "2040-05-21T10:30:00.000Z";
|
||||
const bookingEventTypeId = eventTypeId;
|
||||
const bookingTimeZone = "Europe/London";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {};
|
||||
const bookingResponses = {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [],
|
||||
};
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
@@ -80,10 +111,11 @@ describe("Bookings Endpoints", () => {
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings")
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
@@ -91,8 +123,8 @@ describe("Bookings Endpoints", () => {
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(userEmail);
|
||||
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.uid).toBeDefined();
|
||||
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||
@@ -106,10 +138,10 @@ describe("Bookings Endpoints", () => {
|
||||
|
||||
it("should get bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/bookings")
|
||||
.get("/v2/bookings?filters[status]=upcoming")
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getAllUserBookings>>> =
|
||||
response.body;
|
||||
console.log("asap responseBody", JSON.stringify(response.body, null, 2));
|
||||
const responseBody: GetBookingsOutput = response.body;
|
||||
const fetchedBooking = responseBody.data.bookings[0];
|
||||
|
||||
expect(responseBody.data.bookings.length).toEqual(1);
|
||||
@@ -121,16 +153,16 @@ describe("Bookings Endpoints", () => {
|
||||
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
||||
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
||||
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
||||
expect(fetchedBooking.user?.email).toEqual(createdBooking.user.email);
|
||||
expect(fetchedBooking.user?.email).toEqual(userEmail);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
||||
.get(`/v2/bookings/${createdBooking.uid}`)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>> = response.body;
|
||||
const { bookingInfo } = responseBody.data;
|
||||
const responseBody: GetBookingOutput = response.body;
|
||||
const bookingInfo = responseBody.data;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
@@ -143,68 +175,71 @@ describe("Bookings Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a recurring booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingRecurringCount = 5;
|
||||
const currentBookingRecurringIndex = 0;
|
||||
// note(Lauris) : found this test broken here - first thing to fix is that recurring endpoint accepts an array not 1 object.
|
||||
// it("should create a recurring booking", async () => {
|
||||
// const bookingStart = "2040-05-25T09:30:00.000Z";
|
||||
// const bookingEnd = "2040-05-25T10:30:00.000Z";
|
||||
// const bookingEventTypeId = 7;
|
||||
// const bookingTimeZone = "Europe/London";
|
||||
// const bookingLanguage = "en";
|
||||
// const bookingHashedLink = "";
|
||||
// const bookingRecurringCount = 5;
|
||||
// const currentBookingRecurringIndex = 0;
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
recurringCount: bookingRecurringCount,
|
||||
currentRecurringIndex: currentBookingRecurringIndex,
|
||||
};
|
||||
// const body = {
|
||||
// start: bookingStart,
|
||||
// end: bookingEnd,
|
||||
// eventTypeId: bookingEventTypeId,
|
||||
// timeZone: bookingTimeZone,
|
||||
// language: bookingLanguage,
|
||||
// metadata: {},
|
||||
// hashedLink: bookingHashedLink,
|
||||
// recurringCount: bookingRecurringCount,
|
||||
// currentRecurringIndex: currentBookingRecurringIndex,
|
||||
// };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/reccuring")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
|
||||
response.body;
|
||||
// return request(app.getHttpServer())
|
||||
// .post("/v2/bookings/recurring")
|
||||
// .send(body)
|
||||
// .expect(201)
|
||||
// .then((response) => {
|
||||
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
|
||||
// response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("recurring");
|
||||
});
|
||||
});
|
||||
// expect(responseBody.status).toEqual("recurring");
|
||||
// });
|
||||
// });
|
||||
|
||||
it("should create an instant booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
// note(Lauris) : found this test broken here - first thing to fix is that the eventTypeId must be team event type, because
|
||||
// instant bookings only work for teams.
|
||||
// it("should create an instant booking", async () => {
|
||||
// const bookingStart = "2040-05-25T09:30:00.000Z";
|
||||
// const bookingEnd = "2040-25T10:30:00.000Z";
|
||||
// const bookingEventTypeId = 7;
|
||||
// const bookingTimeZone = "Europe/London";
|
||||
// const bookingLanguage = "en";
|
||||
// const bookingHashedLink = "";
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
};
|
||||
// const body = {
|
||||
// start: bookingStart,
|
||||
// end: bookingEnd,
|
||||
// eventTypeId: bookingEventTypeId,
|
||||
// timeZone: bookingTimeZone,
|
||||
// language: bookingLanguage,
|
||||
// metadata: {},
|
||||
// hashedLink: bookingHashedLink,
|
||||
// };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/instant")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
|
||||
// return request(app.getHttpServer())
|
||||
// .post("/v2/bookings/instant")
|
||||
// .send(body)
|
||||
// .expect(201)
|
||||
// .then((response) => {
|
||||
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("instant");
|
||||
});
|
||||
});
|
||||
// expect(responseBody.status).toEqual("instant");
|
||||
// });
|
||||
// });
|
||||
|
||||
it("should cancel a booking", async () => {
|
||||
const bookingId = createdBooking.id;
|
||||
@@ -215,7 +250,7 @@ describe("Bookings Endpoints", () => {
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
||||
.post(`/v2/bookings/${bookingId}/cancel`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
||||
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
@@ -13,6 +17,7 @@ import {
|
||||
Req,
|
||||
InternalServerErrorException,
|
||||
Body,
|
||||
Headers,
|
||||
HttpException,
|
||||
Param,
|
||||
Get,
|
||||
@@ -20,11 +25,13 @@ import {
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
getBookingInfo,
|
||||
@@ -38,30 +45,58 @@ import {
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { GetBookingsInput, CancelBookingInput } from "@calcom/platform-types";
|
||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type BookingRequest = Request & {
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
type OAuthRequestParams = {
|
||||
platformClientId: string;
|
||||
platformRescheduleUrl: string;
|
||||
platformCancelUrl: string;
|
||||
platformBookingUrl: string;
|
||||
platformBookingLocation?: string;
|
||||
arePlatformEmailsEnabled: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_PLATFORM_PARAMS = {
|
||||
platformClientId: "",
|
||||
platformCancelUrl: "",
|
||||
platformRescheduleUrl: "",
|
||||
platformBookingUrl: "",
|
||||
arePlatformEmailsEnabled: false,
|
||||
platformBookingLocation: undefined,
|
||||
};
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
path: "/bookings",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Bookings")
|
||||
export class BookingsController {
|
||||
private readonly logger = new Logger("ee bookings controller");
|
||||
private readonly logger = new Logger("BookingsController");
|
||||
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||
private readonly billingService: BillingService
|
||||
) {}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/")
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@Permissions([BOOKING_READ])
|
||||
@ApiQuery({ name: "filters[status]", enum: Status, required: true })
|
||||
@ApiQuery({ name: "limit", type: "number", required: false })
|
||||
@ApiQuery({ name: "cursor", type: "number", required: false })
|
||||
async getBookings(
|
||||
@GetUser() user: User,
|
||||
@Query() queryParams: GetBookingsInput
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
): Promise<GetBookingsOutput> {
|
||||
const { filters, cursor, limit } = queryParams;
|
||||
const bookings = await getAllUserBookings({
|
||||
bookingListingByStatus: filters.status,
|
||||
@@ -80,9 +115,8 @@ export class BookingsController {
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid")
|
||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<GetBookingOutput> {
|
||||
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||
|
||||
if (!bookingInfo) {
|
||||
@@ -95,7 +129,6 @@ export class BookingsController {
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid/reschedule")
|
||||
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const booking = await getBookingForReschedule(bookingUid);
|
||||
@@ -111,42 +144,46 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async createBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateBookingInput
|
||||
@Req() req: BookingRequest,
|
||||
@Body() body: CreateBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
req.userId = await this.getOwnerId(req);
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const locationUrl = body.locationUrl;
|
||||
try {
|
||||
const booking = await handleNewBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
const booking = await handleNewBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
|
||||
);
|
||||
|
||||
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err);
|
||||
this.handleBookingErrors(err);
|
||||
}
|
||||
throw new InternalServerErrorException("Could not create booking.");
|
||||
}
|
||||
|
||||
@Post("/:bookingId/cancel")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async cancelBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() body: CancelBookingInput
|
||||
@Body() _: CancelBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
req.userId = await this.getOwnerId(req);
|
||||
req.body = { ...body, id: parseInt(bookingId) };
|
||||
try {
|
||||
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
req.body.id = parseInt(bookingId);
|
||||
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err);
|
||||
this.handleBookingErrors(err);
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException("Booking ID is required.");
|
||||
@@ -154,51 +191,56 @@ export class BookingsController {
|
||||
throw new InternalServerErrorException("Could not cancel booking.");
|
||||
}
|
||||
|
||||
@Post("/reccuring")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async createReccuringBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateReccuringBookingInput[]
|
||||
@Post("/recurring")
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateRecurringBookingInput[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
req.userId = await this.getOwnerId(req);
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
const oAuthClientId = clientId?.toString();
|
||||
try {
|
||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||
req as unknown as NextApiRequest & { userId?: number }
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
|
||||
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: createdBookings,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err, "recurring");
|
||||
this.handleBookingErrors(err, "recurring");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not create recurring booking.");
|
||||
}
|
||||
|
||||
@Post("/instant")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async createInstantBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateBookingInput
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
req.userId = await this.getOwnerId(req);
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
const oAuthClientId = clientId?.toString();
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
req as unknown as NextApiRequest & { userId?: number }
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
|
||||
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: instantMeeting,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err, "instant");
|
||||
this.handleBookingErrors(err, "instant");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not create instant booking.");
|
||||
}
|
||||
|
||||
async getOwnerId(req: Request): Promise<number | undefined> {
|
||||
private async getOwnerId(req: Request): Promise<number | undefined> {
|
||||
try {
|
||||
const accessToken = req.get("Authorization")?.replace("Bearer ", "");
|
||||
if (accessToken) {
|
||||
@@ -208,19 +250,52 @@ export class BookingsController {
|
||||
this.logger.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
|
||||
if (err instanceof HttpError) {
|
||||
const httpError = err as HttpError;
|
||||
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
|
||||
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
|
||||
const res = DEFAULT_PLATFORM_PARAMS;
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
// fetch oAuthClient from db and use data stored in db to set these values
|
||||
if (client) {
|
||||
res.platformClientId = clientId;
|
||||
res.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
|
||||
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
const error = err as Error;
|
||||
throw new InternalServerErrorException(error?.message ?? errMsg);
|
||||
private async createNextApiBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
|
||||
req.body = { ...req.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(errMsg);
|
||||
private handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
|
||||
if (err instanceof HttpError) {
|
||||
const httpError = err as HttpError;
|
||||
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
const error = err as Error;
|
||||
throw new InternalServerErrorException(error?.message ?? errMsg);
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsBoolean, IsTimeZone, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsTimeZone,
|
||||
IsNumber,
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsObject,
|
||||
IsEmail,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
guests!: string[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
location?: Location;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateBookingInput {
|
||||
@IsString()
|
||||
@@ -42,6 +81,7 @@ export class CreateBookingInput {
|
||||
@IsOptional()
|
||||
bookingUid?: string;
|
||||
|
||||
@IsObject()
|
||||
metadata!: Record<string, string>;
|
||||
|
||||
@IsBoolean()
|
||||
@@ -55,4 +95,11 @@ export class CreateBookingInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
seatReferenceUid?: string;
|
||||
|
||||
@Type(() => Response)
|
||||
responses!: Response;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
locationUrl?: string;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { IsBoolean, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
|
||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateReccuringBookingInput extends CreateBookingInput {
|
||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
noEmail?: boolean;
|
||||
@@ -0,0 +1,173 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsObject,
|
||||
ValidateNested,
|
||||
IsArray,
|
||||
IsUrl,
|
||||
IsDateString,
|
||||
IsEmail,
|
||||
} from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Metadata {
|
||||
@IsUrl()
|
||||
videoCallUrl!: string;
|
||||
}
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
notes!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
guests!: string[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
location!: Location;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class Attendee {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
eventName!: string | null;
|
||||
|
||||
@IsString()
|
||||
slug!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class GetBookingData {
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
uid!: string;
|
||||
|
||||
@IsString()
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
customInputs!: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
smsReminderNumber!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsDateString()
|
||||
startTime!: Date;
|
||||
|
||||
@IsDateString()
|
||||
endTime!: Date;
|
||||
|
||||
@IsUrl()
|
||||
location!: string | null;
|
||||
|
||||
@IsString()
|
||||
status!: string;
|
||||
|
||||
metadata!: Metadata | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cancellationReason!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Response)
|
||||
responses!: Response | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
rejectionReason!: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
userPrimaryEmail!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
user!: User | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
attendees!: Attendee[];
|
||||
|
||||
@IsInt()
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
eventType!: EventType | null;
|
||||
}
|
||||
|
||||
export class GetBookingOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: GetBookingData,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => GetBookingData)
|
||||
data!: GetBookingData;
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsBoolean,
|
||||
IsUrl,
|
||||
IsOptional,
|
||||
IsObject,
|
||||
ValidateNested,
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsEmail,
|
||||
} from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
enum Status {
|
||||
CANCELLED = "CANCELLED",
|
||||
REJECTED = "REJECTED",
|
||||
ACCEPTED = "ACCEPTED",
|
||||
PENDING = "PENDING",
|
||||
AWAITING_HOST = "AWAITING_HOST",
|
||||
}
|
||||
|
||||
class Attendee {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
timeZone!: string;
|
||||
|
||||
@IsString()
|
||||
locale!: string | null;
|
||||
|
||||
@IsInt()
|
||||
bookingId!: number | null;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
id?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
eventName?: string | null;
|
||||
|
||||
@IsInt()
|
||||
price!: number;
|
||||
|
||||
@IsOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsString()
|
||||
currency!: string;
|
||||
|
||||
@IsObject()
|
||||
metadata!: any;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAttendees?: boolean | undefined | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAvailabilityCount?: boolean | undefined | null;
|
||||
|
||||
@IsOptional()
|
||||
team?: any | null;
|
||||
}
|
||||
|
||||
class Reference {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
meetingId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
thirdPartyRecurringEventId?: string | null;
|
||||
|
||||
@IsString()
|
||||
meetingPassword!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
meetingUrl?: string | null;
|
||||
|
||||
@IsInt()
|
||||
bookingId!: number | null;
|
||||
|
||||
@IsEmail()
|
||||
externalCalendarId!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
deleted?: any;
|
||||
|
||||
@IsInt()
|
||||
credentialId!: number | null;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
}
|
||||
|
||||
class GetBookingsDataEntry {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
userPrimaryEmail?: string | null;
|
||||
|
||||
@IsString()
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
customInputs!: object | any;
|
||||
|
||||
@IsDateString()
|
||||
startTime!: string;
|
||||
|
||||
@IsDateString()
|
||||
endTime!: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
attendees!: Attendee[];
|
||||
|
||||
metadata!: any;
|
||||
|
||||
@IsString()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsUrl()
|
||||
location!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
eventType!: EventType;
|
||||
|
||||
@IsEnum(Status)
|
||||
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
|
||||
|
||||
@IsBoolean()
|
||||
paid!: boolean;
|
||||
|
||||
@IsArray()
|
||||
payment!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Reference)
|
||||
@IsArray()
|
||||
references!: Reference[];
|
||||
|
||||
@IsBoolean()
|
||||
isRecorded!: boolean;
|
||||
|
||||
@IsArray()
|
||||
seatsReferences!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
user!: User | null;
|
||||
|
||||
@IsOptional()
|
||||
rescheduled?: any;
|
||||
}
|
||||
|
||||
class GetBookingsData {
|
||||
@ValidateNested()
|
||||
@Type(() => GetBookingsDataEntry)
|
||||
@IsArray()
|
||||
bookings!: GetBookingsDataEntry[];
|
||||
|
||||
@IsArray()
|
||||
recurringInfo!: any[];
|
||||
|
||||
@IsInt()
|
||||
nextCursor!: number | null;
|
||||
}
|
||||
|
||||
export class GetBookingsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: GetBookingsData,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => GetBookingsData)
|
||||
data!: GetBookingsData;
|
||||
}
|
||||
@@ -1,30 +1,29 @@
|
||||
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
|
||||
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Controller, Get, Logger, UseGuards, Query } from "@nestjs/common";
|
||||
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
||||
import { CalendarBusyTimesInput } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { EventBusyDate } from "@calcom/types/Calendar";
|
||||
|
||||
@Controller({
|
||||
path: "ee/calendars",
|
||||
path: "/calendars",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@DocsTags("Calendars")
|
||||
export class CalendarsController {
|
||||
private readonly logger = new Logger("ee overlay calendars controller");
|
||||
|
||||
constructor(private readonly calendarsService: CalendarsService) {}
|
||||
|
||||
@Get("/busy-times")
|
||||
async getBusyTimes(
|
||||
@Query() queryParams: CalendarBusyTimesInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<EventBusyDate[]>> {
|
||||
): Promise<GetBusyTimesOutput> {
|
||||
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
|
||||
if (!dateFrom || !dateTo) {
|
||||
return {
|
||||
@@ -48,7 +47,7 @@ export class CalendarsController {
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
async getCalendars(@GetUser("id") userId: number): Promise<ApiResponse<ConnectedDestinationCalendars>> {
|
||||
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
|
||||
const calendars = await this.calendarsService.getCalendars(userId);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class BusyTimesOutput {
|
||||
@IsDate()
|
||||
start!: Date;
|
||||
|
||||
@IsDate()
|
||||
end!: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
source?: string | null;
|
||||
}
|
||||
|
||||
export class GetBusyTimesOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => BusyTimesOutput)
|
||||
@IsArray()
|
||||
data!: BusyTimesOutput[];
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Integration {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
appData?: object | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dirName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
__template?: string;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
description!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
installed?: boolean;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
variant!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
category?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
categories!: string[];
|
||||
|
||||
@IsString()
|
||||
logo!: string;
|
||||
|
||||
@IsString()
|
||||
publisher!: string;
|
||||
|
||||
@IsString()
|
||||
slug!: string;
|
||||
|
||||
@IsUrl()
|
||||
url!: string;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
locationOption!: object | null;
|
||||
}
|
||||
|
||||
class Primary {
|
||||
@IsEmail()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
primary!: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
class Calendar {
|
||||
@IsEmail()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
primary?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
class ConnectedCalendar {
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
integration!: Integration;
|
||||
|
||||
@IsInt()
|
||||
credentialId!: number;
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
primary?: Primary;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
calendars?: Calendar[];
|
||||
}
|
||||
|
||||
class DestinationCalendar {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
integration!: string;
|
||||
|
||||
@IsEmail()
|
||||
externalId!: string;
|
||||
|
||||
@IsEmail()
|
||||
primaryEmail!: string | null;
|
||||
|
||||
@IsInt()
|
||||
userId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
credentialId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
primary?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
readOnly?: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
integrationTitle?: string;
|
||||
}
|
||||
|
||||
class ConnectedCalendarsData {
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
connectedCalendars!: ConnectedCalendar[];
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
destinationCalendar!: DestinationCalendar;
|
||||
}
|
||||
export class ConnectedCalendarsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => ConnectedCalendarsData)
|
||||
data!: ConnectedCalendarsData;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CredentialsWithUserEmail,
|
||||
} from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Injectable,
|
||||
@@ -23,7 +24,8 @@ export class CalendarsService {
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly dbRead: PrismaReadService
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async getCalendars(userId: number) {
|
||||
@@ -35,7 +37,7 @@ export class CalendarsService {
|
||||
return getConnectedDestinationCalendars(
|
||||
userWithCalendars,
|
||||
false,
|
||||
this.dbRead.prisma as unknown as PrismaClient
|
||||
this.dbWrite.prisma as unknown as PrismaClient
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import { AppModule } from "@/app.module";
|
||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
@@ -63,7 +66,7 @@ describe("Event types Endpoints", () => {
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "event-types-test-e2e@api.com";
|
||||
const name = "bob the builder";
|
||||
const name = "bob-the-builder";
|
||||
const username = name;
|
||||
let eventType: EventType;
|
||||
let user: User;
|
||||
@@ -172,14 +175,49 @@ describe("Event types Endpoints", () => {
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventType> = response.body;
|
||||
const responseBody: GetEventTypeOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toEqual(eventType.id);
|
||||
expect(responseBody.data.title).toEqual(eventType.title);
|
||||
expect(responseBody.data.slug).toEqual(eventType.slug);
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
expect(responseBody.data.eventType.id).toEqual(eventType.id);
|
||||
expect(responseBody.data.eventType.title).toEqual(eventType.title);
|
||||
expect(responseBody.data.eventType.slug).toEqual(eventType.slug);
|
||||
expect(responseBody.data.eventType.userId).toEqual(user.id);
|
||||
});
|
||||
|
||||
it(`/GET/:username/public`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetEventTypesPublicOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
expect(responseBody.data?.[0]?.id).toEqual(eventType.id);
|
||||
expect(responseBody.data?.[0]?.title).toEqual(eventType.title);
|
||||
expect(responseBody.data?.[0]?.slug).toEqual(eventType.slug);
|
||||
expect(responseBody.data?.[0]?.length).toEqual(eventType.length);
|
||||
});
|
||||
|
||||
it(`/GET/:username/:eventSlug/public`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetEventTypePublicOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.id).toEqual(eventType.id);
|
||||
expect(responseBody.data?.title).toEqual(eventType.title);
|
||||
expect(responseBody.data?.slug).toEqual(eventType.slug);
|
||||
expect(responseBody.data?.length).toEqual(eventType.length);
|
||||
});
|
||||
|
||||
it(`/GET/`, async () => {
|
||||
@@ -201,7 +239,7 @@ describe("Event types Endpoints", () => {
|
||||
expect(responseBody.data.profiles?.[0]?.name).toEqual(name);
|
||||
});
|
||||
|
||||
it(`/GET/:username/public`, async () => {
|
||||
it(`/GET/public/:username/`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${username}/public`)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
||||
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||
import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
||||
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||
import { ForAtom } from "@/lib/atoms/decorators/for-atom.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
@@ -20,19 +27,15 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Delete,
|
||||
Query,
|
||||
InternalServerErrorException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { EventType } from "@prisma/client";
|
||||
|
||||
import { EventTypesByViewer } from "@calcom/lib";
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type {
|
||||
EventType as AtomEventType,
|
||||
EventTypesPublic,
|
||||
UpdateEventTypeReturn,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { getPublicEvent } from "@calcom/platform-libraries";
|
||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||
import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
path: "event-types",
|
||||
@@ -41,7 +44,10 @@ import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Event types")
|
||||
export class EventTypesController {
|
||||
constructor(private readonly eventTypesService: EventTypesService) {}
|
||||
constructor(
|
||||
private readonly eventTypesService: EventTypesService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@@ -63,12 +69,9 @@ export class EventTypesController {
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getEventType(
|
||||
@Param("eventTypeId") eventTypeId: string,
|
||||
@ForAtom() forAtom: boolean,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiSuccessResponse<EventType | AtomEventType>> {
|
||||
const eventType = forAtom
|
||||
? await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId))
|
||||
: await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
|
||||
): Promise<GetEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
||||
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
@@ -83,7 +86,7 @@ export class EventTypesController {
|
||||
@Get("/")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<ApiSuccessResponse<EventTypesByViewer>> {
|
||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
|
||||
const eventTypes = await getEventTypesByViewer({
|
||||
id: user.id,
|
||||
profile: {
|
||||
@@ -93,15 +96,41 @@ export class EventTypesController {
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventTypes,
|
||||
data: eventTypes as GetEventTypesData,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:username/:eventSlug/public")
|
||||
async getPublicEventType(
|
||||
@Param("username") username: string,
|
||||
@Param("eventSlug") eventSlug: string,
|
||||
@Query() queryParams: GetPublicEventTypeQueryParams
|
||||
): Promise<GetEventTypePublicOutput> {
|
||||
try {
|
||||
const event = await getPublicEvent(
|
||||
username.toLowerCase(),
|
||||
eventSlug,
|
||||
queryParams.isTeamEvent,
|
||||
queryParams.org || null,
|
||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||
true
|
||||
);
|
||||
return {
|
||||
data: event,
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new NotFoundException(err.message);
|
||||
}
|
||||
}
|
||||
throw new InternalServerErrorException("Could not find public event.");
|
||||
}
|
||||
|
||||
@Get("/:username/public")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
async getPublicEventTypes(
|
||||
@Param("username") username: string
|
||||
): Promise<ApiSuccessResponse<EventTypesPublic>> {
|
||||
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
||||
|
||||
return {
|
||||
@@ -118,7 +147,7 @@ export class EventTypesController {
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@Body() body: UpdateEventTypeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn["eventType"]>> {
|
||||
): Promise<UpdateEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||
|
||||
return {
|
||||
@@ -133,12 +162,17 @@ export class EventTypesController {
|
||||
async deleteEventType(
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@GetUser("id") userId: number
|
||||
): Promise<ApiResponse<EventType>> {
|
||||
): Promise<DeleteEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
data: {
|
||||
id: eventType.id,
|
||||
length: eventType.length,
|
||||
slug: eventType.slug,
|
||||
title: eventType.title,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesRepository {
|
||||
@@ -44,19 +45,14 @@ export class EventTypesRepository {
|
||||
isUserOrganizationAdmin: boolean,
|
||||
eventTypeId: number
|
||||
) {
|
||||
try {
|
||||
return getEventTypeById({
|
||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
||||
eventTypeId,
|
||||
userId: user.id,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbRead.prisma,
|
||||
isUserOrganizationAdmin,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new NotFoundException(`User with id ${user.id} has no event type with id ${eventTypeId}`);
|
||||
}
|
||||
return await getEventTypeById({
|
||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
||||
eventTypeId,
|
||||
userId: user.id,
|
||||
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||
isUserOrganizationAdmin,
|
||||
isTrpcCall: true,
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeById(eventTypeId: number) {
|
||||
|
||||
+1
-10
@@ -2,16 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class GetPublicEventInput {
|
||||
@IsString()
|
||||
@Transform(({ value }: { value: string }) => value.toLowerCase())
|
||||
@ApiProperty({ required: true })
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ required: true })
|
||||
eventSlug!: string;
|
||||
|
||||
export class GetPublicEventTypeQueryParams {
|
||||
@Transform(({ value }: { value: string }) => value === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsInt, IsString } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class DeleteData {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
}
|
||||
|
||||
export class DeleteEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Type(() => DeleteData)
|
||||
data!: DeleteData;
|
||||
}
|
||||
@@ -55,11 +55,12 @@ export class EventTypeOutput {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation)
|
||||
@IsArray()
|
||||
locations!: EventTypeLocation | null;
|
||||
locations!: EventTypeLocation[] | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
position!: number;
|
||||
@IsOptional()
|
||||
position?: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
@@ -71,7 +72,8 @@ export class EventTypeOutput {
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
profileId!: number | null;
|
||||
@IsOptional()
|
||||
profileId?: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
@@ -83,7 +85,8 @@ export class EventTypeOutput {
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
parentId!: number | null;
|
||||
@IsOptional()
|
||||
parentId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@@ -176,7 +179,8 @@ export class EventTypeOutput {
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
scheduleId!: number | null;
|
||||
@IsOptional()
|
||||
scheduleId?: number | null;
|
||||
|
||||
@IsNumber()
|
||||
@ApiHideProperty()
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateNested,
|
||||
IsArray,
|
||||
IsObject,
|
||||
IsNumber,
|
||||
IsEnum,
|
||||
} from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
type!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class OptionInput {
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class BookingField {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
optionsInputs?: { [key: string]: OptionInput };
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
editable?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@IsOptional()
|
||||
sources?: Source[];
|
||||
}
|
||||
|
||||
class Organization {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
slug?: string | null;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: Record<string, any>;
|
||||
}
|
||||
|
||||
class Profile {
|
||||
@IsString()
|
||||
username!: string | null;
|
||||
|
||||
@IsInt()
|
||||
id!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
userId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
uid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsInt()
|
||||
organizationId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Organization)
|
||||
organization?: Organization | null;
|
||||
|
||||
@IsString()
|
||||
upId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
image?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
brandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
darkBrandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
bookerLayouts?: any;
|
||||
}
|
||||
|
||||
class Owner {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@IsString()
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
brandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
darkBrandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
theme?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: any;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
defaultScheduleId?: number | null;
|
||||
|
||||
@IsString()
|
||||
nonProfileUsername!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Profile)
|
||||
profile!: Profile;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsString()
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
weekStart!: string;
|
||||
|
||||
@IsInt()
|
||||
organizationId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
profile!: Profile;
|
||||
|
||||
@IsString()
|
||||
bookerUrl!: string;
|
||||
}
|
||||
|
||||
class Schedule {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class PublicEventTypeOutput {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
description!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
eventName?: string | null;
|
||||
|
||||
@IsString()
|
||||
slug!: string;
|
||||
|
||||
@IsBoolean()
|
||||
isInstantEvent!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
aiPhoneCallConfig?: any;
|
||||
|
||||
@IsOptional()
|
||||
schedulingType?: any;
|
||||
|
||||
@IsInt()
|
||||
length!: number;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Location)
|
||||
locations!: Location[];
|
||||
|
||||
@IsArray()
|
||||
customInputs!: any[];
|
||||
|
||||
@IsBoolean()
|
||||
disableGuests!: boolean;
|
||||
|
||||
@IsObject()
|
||||
metadata!: object | null;
|
||||
|
||||
@IsBoolean()
|
||||
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
requiresConfirmation!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
requiresBookerEmailVerification!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsNumber()
|
||||
price!: number;
|
||||
|
||||
@IsString()
|
||||
currency!: string;
|
||||
|
||||
@IsOptional()
|
||||
seatsPerTimeSlot?: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
seatsShowAvailabilityCount!: boolean | null;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField)
|
||||
bookingFields!: BookingField[];
|
||||
|
||||
@IsOptional()
|
||||
team?: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
successRedirectUrl?: string | null;
|
||||
|
||||
@IsArray()
|
||||
workflows!: any[];
|
||||
|
||||
@IsArray()
|
||||
hosts!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Owner)
|
||||
owner!: Owner | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Schedule)
|
||||
schedule!: Schedule | null;
|
||||
|
||||
@IsBoolean()
|
||||
hidden!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
assignAllTeamMembers!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
bookerLayouts?: any;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => User)
|
||||
users!: User[];
|
||||
|
||||
@IsObject()
|
||||
entity!: object;
|
||||
|
||||
@IsBoolean()
|
||||
isDynamic!: boolean;
|
||||
}
|
||||
|
||||
export class GetEventTypePublicOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PublicEventTypeOutput)
|
||||
@IsArray()
|
||||
data!: PublicEventTypeOutput | null;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Data {
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
eventType!: EventTypeOutput;
|
||||
}
|
||||
|
||||
export class GetEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: Data,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Data)
|
||||
data!: Data;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class PublicEventType {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export class GetEventTypesPublicOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PublicEventType)
|
||||
@IsArray()
|
||||
data!: PublicEventType[];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class EventTypeGroup {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeOutput)
|
||||
@IsArray()
|
||||
eventTypes!: EventTypeOutput[];
|
||||
}
|
||||
|
||||
export class GetEventTypesData {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeGroup)
|
||||
@IsArray()
|
||||
eventTypeGroups!: EventTypeGroup[];
|
||||
}
|
||||
|
||||
export class GetEventTypesOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => GetEventTypesData)
|
||||
@IsArray()
|
||||
data!: GetEventTypesData;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UpdateEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
data!: EventTypeOutput;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
|
||||
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
@@ -22,7 +23,7 @@ export class EventTypesService {
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput) {
|
||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput): Promise<EventTypeOutput> {
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const { eventType } = await createEventType({
|
||||
@@ -34,7 +35,7 @@ export class EventTypesService {
|
||||
prisma: this.dbWrite.prisma,
|
||||
},
|
||||
});
|
||||
return eventType;
|
||||
return eventType as EventTypeOutput;
|
||||
}
|
||||
|
||||
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) {
|
||||
@@ -88,7 +89,7 @@ export class EventTypesService {
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||
return eventType;
|
||||
return eventType as { eventType: EventTypeOutput };
|
||||
}
|
||||
|
||||
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
||||
@@ -101,11 +102,7 @@ export class EventTypesService {
|
||||
}
|
||||
|
||||
async createUserDefaultEventTypes(userId: number) {
|
||||
const thirtyMinutes = DEFAULT_EVENT_TYPES.thirtyMinutes;
|
||||
const thirtyMinutesVideo = DEFAULT_EVENT_TYPES.thirtyMinutesVideo;
|
||||
|
||||
const sixtyMinutes = DEFAULT_EVENT_TYPES.sixtyMinutes;
|
||||
const sixtyMinutesVideo = DEFAULT_EVENT_TYPES.sixtyMinutesVideo;
|
||||
const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES;
|
||||
|
||||
const defaultEventTypes = await Promise.all([
|
||||
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
|
||||
@@ -120,7 +117,7 @@ export class EventTypesService {
|
||||
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
|
||||
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
const { eventType } = await updateEventType({
|
||||
await updateEventType({
|
||||
input: { id: eventTypeId, ...body },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
@@ -130,7 +127,13 @@ export class EventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
return eventType;
|
||||
const eventType = await this.getUserEventTypeForAtom(user, eventTypeId);
|
||||
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return eventType.eventType;
|
||||
}
|
||||
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -18,6 +19,20 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||
|
||||
class CalendarsServiceMock {
|
||||
async getCalendars() {
|
||||
return {
|
||||
connectedCalendars: [
|
||||
{
|
||||
integration: {
|
||||
type: "google_calendar",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe("Platform Gcal Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
@@ -32,6 +47,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
let gcalCredentials: Credential;
|
||||
let accessTokenSecret: string;
|
||||
let refreshTokenSecret: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
@@ -41,6 +57,8 @@ describe("Platform Gcal Endpoints", () => {
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.overrideProvider(CalendarsService)
|
||||
.useClass(CalendarsServiceMock)
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
@@ -84,14 +102,14 @@ describe("Platform Gcal Endpoints", () => {
|
||||
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.get(`/v2/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer invalid_access_token`)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.get(`/v2/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
@@ -102,7 +120,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -110,7 +128,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -118,25 +136,25 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check without access token`, async () => {
|
||||
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
|
||||
await request(app.getHttpServer()).get(`/v2/gcal/check`).expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(400);
|
||||
@@ -150,7 +168,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
"google-calendar"
|
||||
);
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { GcalService } from "@/modules/apps/services/gcal.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
|
||||
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
|
||||
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
|
||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
@@ -22,6 +25,7 @@ import {
|
||||
Headers,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Request } from "express";
|
||||
import { google } from "googleapis";
|
||||
import { z } from "zod";
|
||||
@@ -32,30 +36,31 @@ import {
|
||||
GOOGLE_CALENDAR_TYPE,
|
||||
SUCCESS_STATUS,
|
||||
} from "@calcom/platform-constants";
|
||||
import { ApiRedirectResponseType, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
const CALENDAR_SCOPES = [
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
];
|
||||
|
||||
// Controller for the GCalConnect Atom
|
||||
@Controller({
|
||||
path: "ee/gcal",
|
||||
path: "/gcal",
|
||||
version: "2",
|
||||
})
|
||||
@DocsTags("Google Calendar")
|
||||
export class GcalController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
|
||||
constructor(
|
||||
private readonly appRepository: AppsRepository,
|
||||
private readonly credentialRepository: CredentialsRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly config: ConfigService,
|
||||
private readonly gcalService: GcalService
|
||||
private readonly gcalService: GCalService,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
||||
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||
|
||||
@Get("/oauth/auth-url")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@@ -63,7 +68,7 @@ export class GcalController {
|
||||
async redirect(
|
||||
@Headers("Authorization") authorization: string,
|
||||
@Req() req: Request
|
||||
): Promise<ApiResponse<{ authUrl: string }>> {
|
||||
): Promise<GcalAuthUrlOutput> {
|
||||
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
|
||||
const accessToken = authorization.replace("Bearer ", "");
|
||||
const origin = req.get("origin") ?? req.get("host");
|
||||
@@ -79,7 +84,7 @@ export class GcalController {
|
||||
@Get("/oauth/save")
|
||||
@Redirect(undefined, 301)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async save(@Query("state") state: string, @Query("code") code: string): Promise<ApiRedirectResponseType> {
|
||||
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
|
||||
const stateParams = new URLSearchParams(state);
|
||||
const { accessToken, origin } = z
|
||||
.object({ accessToken: z.string(), origin: z.string() })
|
||||
@@ -135,7 +140,7 @@ export class GcalController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@Permissions([APPS_READ])
|
||||
async check(@GetUser("id") userId: number): Promise<ApiResponse> {
|
||||
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
|
||||
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
||||
|
||||
if (!gcalCredentials) {
|
||||
@@ -146,6 +151,17 @@ export class GcalController {
|
||||
throw new BadRequestException("Invalid google oauth credentials.");
|
||||
}
|
||||
|
||||
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
|
||||
const googleCalendar = connectedCalendars.find(
|
||||
(cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE
|
||||
);
|
||||
if (!googleCalendar) {
|
||||
throw new UnauthorizedException("Google Calendar not connected.");
|
||||
}
|
||||
if (googleCalendar.error?.message) {
|
||||
throw new UnauthorizedException(googleCalendar.error?.message);
|
||||
}
|
||||
|
||||
return { status: SUCCESS_STATUS };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GcalController } from "@/ee/gcal/gcal.controller";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { GcalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
||||
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GcalService],
|
||||
providers: [
|
||||
AppsRepository,
|
||||
ConfigService,
|
||||
CredentialsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
GCalService,
|
||||
CalendarsService,
|
||||
UsersRepository,
|
||||
],
|
||||
controllers: [GcalController],
|
||||
})
|
||||
export class GcalModule {}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class AuthUrlData {
|
||||
@IsString()
|
||||
authUrl!: string;
|
||||
}
|
||||
|
||||
export class GcalAuthUrlOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Type(() => AuthUrlData)
|
||||
@ValidateNested()
|
||||
data!: AuthUrlData;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GcalCheckOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class GcalSaveRedirectOutput {
|
||||
@IsString()
|
||||
url!: string;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
@@ -35,8 +34,14 @@ describe("Me Endpoints", () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
||||
providers: [SchedulesRepository, SchedulesService],
|
||||
imports: [
|
||||
AppModule,
|
||||
PrismaModule,
|
||||
AvailabilitiesModule,
|
||||
UsersModule,
|
||||
TokensModule,
|
||||
SchedulesModule,
|
||||
],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
@@ -50,6 +55,7 @@ describe("Me Endpoints", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
@@ -65,7 +71,7 @@ describe("Me Endpoints", () => {
|
||||
|
||||
it("should get user associated with access token", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/me")
|
||||
.get("/v2/me")
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
||||
@@ -81,10 +87,10 @@ describe("Me Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should update user associated with access token", async () => {
|
||||
const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" };
|
||||
const body: UpdateManagedUserInput = { timeZone: "Europe/Rome" };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/me")
|
||||
.patch("/v2/me")
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -106,24 +112,21 @@ describe("Me Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid timezone", async () => {
|
||||
const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" };
|
||||
const bodyWithIncorrectTimeZone: UpdateManagedUserInput = { timeZone: "Narnia/Woods" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid time format", async () => {
|
||||
const bodyWithIncorrectTimeFormat: UpdateManagedPlatformUserInput = { timeFormat: 100 };
|
||||
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/me")
|
||||
.send(bodyWithIncorrectTimeFormat)
|
||||
.expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid week start", async () => {
|
||||
const bodyWithIncorrectWeekStart: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" };
|
||||
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
|
||||
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { UserResponse, userSchemaResponse } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { userSchemaResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "ee/me",
|
||||
path: "/me",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@DocsTags("Me")
|
||||
export class MeController {
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepository,
|
||||
@@ -24,7 +27,7 @@ export class MeController {
|
||||
|
||||
@Get("/")
|
||||
@Permissions([PROFILE_READ])
|
||||
async getMe(@GetUser() user: UserWithProfile): Promise<ApiResponse<UserResponse>> {
|
||||
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
|
||||
const me = userSchemaResponse.parse(user);
|
||||
|
||||
return {
|
||||
@@ -37,11 +40,11 @@ export class MeController {
|
||||
@Permissions([PROFILE_WRITE])
|
||||
async updateMe(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() bodySchedule: UpdateManagedPlatformUserInput
|
||||
): Promise<ApiResponse<UserResponse>> {
|
||||
@Body() bodySchedule: UpdateManagedUserInput
|
||||
): Promise<UpdateMeOutput> {
|
||||
const updatedUser = await this.usersRepository.update(user.id, bodySchedule);
|
||||
if (bodySchedule.timeZone && user.defaultScheduleId) {
|
||||
await this.schedulesRepository.updateUserSchedule(user.id, user.defaultScheduleId, {
|
||||
await this.schedulesRepository.updateUserSchedule(user, user.defaultScheduleId, {
|
||||
timeZone: bodySchedule.timeZone,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MeOutput } from "@/ee/me/outputs/me.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetMeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: MeOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => MeOutput)
|
||||
data!: MeOutput;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IsInt, IsEmail, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class MeOutput {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsInt()
|
||||
timeFormat!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
defaultScheduleId!: number | null;
|
||||
|
||||
@IsString()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
timeZone!: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MeOutput } from "@/ee/me/outputs/me.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UpdateMeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: MeOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => MeOutput)
|
||||
data!: MeOutput;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class ProviderVerifyAccessTokenOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class ProviderVerifyClientOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
|
||||
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
BadRequestException,
|
||||
@@ -9,31 +10,26 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Param,
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "ee/provider",
|
||||
path: "/provider",
|
||||
version: "2",
|
||||
})
|
||||
@DocsTags("Cal provider")
|
||||
export class CalProviderController {
|
||||
private readonly logger = new Logger("Platform Provider Controller");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly oauthClientRepository: OAuthClientRepository
|
||||
) {}
|
||||
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
|
||||
|
||||
@Get("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async verifyClientId(@Param("clientId") clientId: string): Promise<ApiResponse> {
|
||||
async verifyClientId(@Param("clientId") clientId: string): Promise<ProviderVerifyClientOutput> {
|
||||
if (!clientId) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@@ -52,7 +48,7 @@ export class CalProviderController {
|
||||
async verifyAccessToken(
|
||||
@Param("clientId") clientId: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse> {
|
||||
): Promise<ProviderVerifyAccessTokenOutput> {
|
||||
if (!clientId) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
||||
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
||||
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -17,13 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
ScheduleWithAvailabilities,
|
||||
ScheduleWithAvailabilitiesForWeb,
|
||||
UpdateScheduleOutputType,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
||||
|
||||
describe("Schedules Endpoints", () => {
|
||||
describe("User Authentication", () => {
|
||||
@@ -35,14 +32,23 @@ describe("Schedules Endpoints", () => {
|
||||
const userEmail = "schedules-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let createdSchedule: ScheduleResponse;
|
||||
let createdSchedule: CreateScheduleOutput["data"];
|
||||
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
||||
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
|
||||
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
||||
providers: [SchedulesRepository, SchedulesService],
|
||||
imports: [
|
||||
AppModule,
|
||||
PrismaModule,
|
||||
AvailabilitiesModule,
|
||||
UsersModule,
|
||||
TokensModule,
|
||||
SchedulesModule,
|
||||
],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
@@ -71,10 +77,12 @@ describe("Schedules Endpoints", () => {
|
||||
it("should create a default schedule", async () => {
|
||||
const scheduleName = "schedule-name";
|
||||
const scheduleTimeZone = "Europe/Rome";
|
||||
const isDefault = true;
|
||||
|
||||
const body = {
|
||||
const body: CreateScheduleInput = {
|
||||
name: scheduleName,
|
||||
timeZone: scheduleTimeZone,
|
||||
isDefault,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -82,73 +90,25 @@ describe("Schedules Endpoints", () => {
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
expect(responseBody.data.name).toEqual(scheduleName);
|
||||
expect(responseBody.data.timeZone).toEqual(scheduleTimeZone);
|
||||
const responseData: CreateScheduleOutput = response.body;
|
||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseData.data).toBeDefined();
|
||||
expect(responseData.data.isDefault).toEqual(isDefault);
|
||||
expect(responseData.data.timeZone).toEqual(scheduleTimeZone);
|
||||
expect(responseData.data.name).toEqual(scheduleName);
|
||||
|
||||
expect(responseBody.data.availability).toBeDefined();
|
||||
expect(responseBody.data.availability?.length).toEqual(1);
|
||||
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
||||
const defaultAvailabilityStartTime = "09:00:00";
|
||||
const defaultAvailabilityEndTime = "17:00:00";
|
||||
const schedule = responseData.data.schedule;
|
||||
expect(schedule).toBeDefined();
|
||||
expect(schedule.length).toEqual(1);
|
||||
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||
|
||||
expect(responseBody.data.availability?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||
|
||||
const scheduleUser = await userRepositoryFixture.get(responseBody.data.userId);
|
||||
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.id);
|
||||
await scheduleRepositoryFixture.deleteById(responseBody.data.id);
|
||||
await scheduleRepositoryFixture.deleteAvailabilities(responseBody.data.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a schedule", async () => {
|
||||
const scheduleName = "schedule-name";
|
||||
const scheduleTimeZone = "Europe/Rome";
|
||||
const availabilityDays = [1, 2, 3, 4, 5, 6];
|
||||
const availabilityStartTime = "11:00:00";
|
||||
const availabilityEndTime = "14:00:00";
|
||||
|
||||
const body = {
|
||||
name: scheduleName,
|
||||
timeZone: scheduleTimeZone,
|
||||
availabilities: [
|
||||
{
|
||||
days: availabilityDays,
|
||||
startTime: availabilityStartTime,
|
||||
endTime: availabilityEndTime,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/schedules")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
expect(responseBody.data.name).toEqual(scheduleName);
|
||||
expect(responseBody.data.timeZone).toEqual(scheduleTimeZone);
|
||||
|
||||
expect(responseBody.data.availability).toBeDefined();
|
||||
expect(responseBody.data.availability?.length).toEqual(1);
|
||||
expect(responseBody.data.availability?.[0]?.days).toEqual(availabilityDays);
|
||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(availabilityStartTime);
|
||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(availabilityEndTime);
|
||||
|
||||
createdSchedule = responseBody.data;
|
||||
|
||||
const scheduleUser = await userRepositoryFixture.get(responseBody.data.userId);
|
||||
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.id);
|
||||
const scheduleUser = schedule?.[0].userId
|
||||
? await userRepositoryFixture.get(schedule?.[0].userId)
|
||||
: null;
|
||||
expect(scheduleUser?.defaultScheduleId).toEqual(responseData.data.id);
|
||||
createdSchedule = responseData.data;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,90 +116,19 @@ describe("Schedules Endpoints", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/schedules/default")
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.userId).toEqual(createdSchedule.userId);
|
||||
expect(responseBody.data.name).toEqual(createdSchedule.name);
|
||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
||||
.then(async (response) => {
|
||||
const responseData: CreateScheduleOutput = response.body;
|
||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseData.data).toBeDefined();
|
||||
expect(responseData.data.id).toEqual(createdSchedule.id);
|
||||
expect(responseData.data.schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId);
|
||||
|
||||
expect(responseBody.data.availability).toBeDefined();
|
||||
expect(responseBody.data.availability?.length).toEqual(1);
|
||||
|
||||
expect(responseBody.data.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
|
||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(
|
||||
createdSchedule.availability?.[0]?.startTime
|
||||
);
|
||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(
|
||||
createdSchedule.availability?.[0]?.endTime
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get schedule", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/schedules/${createdSchedule.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(
|
||||
(responseBody.data as unknown as ScheduleWithAvailabilitiesForWeb).dateOverrides
|
||||
).toBeFalsy();
|
||||
expect(responseBody.data.userId).toEqual(createdSchedule.userId);
|
||||
expect(responseBody.data.name).toEqual(createdSchedule.name);
|
||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
||||
|
||||
expect(responseBody.data.availability).toBeDefined();
|
||||
expect(responseBody.data.availability?.length).toEqual(1);
|
||||
|
||||
expect(responseBody.data.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
|
||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(
|
||||
createdSchedule.availability?.[0]?.startTime
|
||||
);
|
||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(
|
||||
createdSchedule.availability?.[0]?.endTime
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get schedule for atom", async () => {
|
||||
const forAtomQueryParam = "?for=atom";
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/schedules/${createdSchedule.id}${forAtomQueryParam}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleWithAvailabilitiesForWeb> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.dateOverrides).toBeDefined();
|
||||
expect((responseBody.data as unknown as ScheduleWithAvailabilities).userId).toBeFalsy();
|
||||
expect(responseBody.data.name).toEqual(createdSchedule.name);
|
||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
||||
|
||||
expect(responseBody.data.availability).toBeDefined();
|
||||
expect(responseBody.data.availability?.length).toEqual(7);
|
||||
|
||||
const dateStart = new Date(responseBody.data.availability?.[1]?.[0]?.start);
|
||||
const dateEnd = new Date(responseBody.data.availability?.[1]?.[0]?.end);
|
||||
|
||||
const dateStartHours = dateStart.getUTCHours().toString().padStart(2, "0");
|
||||
const dateStartMinutes = dateStart.getUTCMinutes().toString().padStart(2, "0");
|
||||
const dateStartSeconds = dateStart.getUTCSeconds().toString().padStart(2, "0");
|
||||
const dateEndHours = dateEnd.getUTCHours().toString().padStart(2, "0");
|
||||
const dateEndMinutes = dateEnd.getUTCMinutes().toString().padStart(2, "0");
|
||||
const dateEndSeconds = dateEnd.getUTCSeconds().toString().padStart(2, "0");
|
||||
|
||||
const expectedStart = `${dateStartHours}:${dateStartMinutes}:${dateStartSeconds}`;
|
||||
const expectedEnd = `${dateEndHours}:${dateEndMinutes}:${dateEndSeconds}`;
|
||||
|
||||
expect(expectedStart).toEqual(createdSchedule.availability?.[0]?.startTime);
|
||||
expect(expectedEnd).toEqual(createdSchedule.availability?.[0]?.endTime);
|
||||
const schedule = responseData.data.schedule;
|
||||
expect(schedule).toBeDefined();
|
||||
expect(schedule.length).toEqual(1);
|
||||
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,77 +137,26 @@ describe("Schedules Endpoints", () => {
|
||||
.get(`/api/v2/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleResponse[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(1);
|
||||
const responseData: GetSchedulesOutput = response.body;
|
||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseData.data).toBeDefined();
|
||||
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
|
||||
expect(responseData.data?.[0].schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId);
|
||||
|
||||
const fetchedSchedule = responseBody.data[0];
|
||||
expect(fetchedSchedule).toBeDefined();
|
||||
expect(fetchedSchedule.userId).toEqual(createdSchedule.userId);
|
||||
expect(fetchedSchedule.name).toEqual(createdSchedule.name);
|
||||
expect(fetchedSchedule.timeZone).toEqual(createdSchedule.timeZone);
|
||||
|
||||
expect(fetchedSchedule.availability).toBeDefined();
|
||||
expect(fetchedSchedule.availability?.length).toEqual(1);
|
||||
|
||||
expect(fetchedSchedule.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
|
||||
expect(fetchedSchedule.availability?.[0]?.startTime).toEqual(
|
||||
createdSchedule.availability?.[0]?.startTime
|
||||
);
|
||||
expect(fetchedSchedule.availability?.[0]?.endTime).toEqual(
|
||||
createdSchedule.availability?.[0]?.endTime
|
||||
);
|
||||
const schedule = responseData.data?.[0].schedule;
|
||||
expect(schedule).toBeDefined();
|
||||
expect(schedule.length).toEqual(1);
|
||||
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update schedule name", async () => {
|
||||
const newScheduleName = "new-schedule-name";
|
||||
|
||||
const schedule = [
|
||||
[],
|
||||
[
|
||||
{
|
||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const body: UpdateScheduleInput = {
|
||||
scheduleId: createdSchedule.id,
|
||||
name: newScheduleName,
|
||||
schedule,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -326,29 +164,19 @@ describe("Schedules Endpoints", () => {
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response: any) => {
|
||||
const responseBody: ApiSuccessResponse<UpdateScheduleOutputType> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.schedule.id).toBeDefined();
|
||||
expect(responseBody.data.schedule.userId).toEqual(createdSchedule.userId);
|
||||
expect(responseBody.data.schedule.name).toEqual(newScheduleName);
|
||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
||||
const responseData: UpdateScheduleOutput = response.body;
|
||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseData.data).toBeDefined();
|
||||
expect(responseData.data.schedule.name).toEqual(newScheduleName);
|
||||
expect(responseData.data.schedule.id).toEqual(createdSchedule.id);
|
||||
expect(responseData.data.schedule.userId).toEqual(createdSchedule.schedule[0].userId);
|
||||
|
||||
expect(responseBody.data.availability).toBeDefined();
|
||||
const availabilities = responseBody.data?.availability?.filter(
|
||||
(availability: any[]) => availability.length
|
||||
);
|
||||
expect(availabilities?.length).toEqual(6);
|
||||
|
||||
expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(
|
||||
createdSchedule.availability?.[0]?.days
|
||||
);
|
||||
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(
|
||||
`1970-01-01T${createdSchedule.availability?.[0]?.startTime}.000Z`
|
||||
);
|
||||
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(
|
||||
`1970-01-01T${createdSchedule.availability?.[0]?.endTime}.000Z`
|
||||
);
|
||||
const availability = responseData.data.schedule.availability;
|
||||
expect(availability).toBeDefined();
|
||||
expect(availability?.length).toEqual(1);
|
||||
expect(availability?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||
expect(availability?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||
expect(availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||
|
||||
createdSchedule.name = newScheduleName;
|
||||
});
|
||||
@@ -360,6 +188,10 @@ describe("Schedules Endpoints", () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
try {
|
||||
await scheduleRepositoryFixture.deleteById(createdSchedule.id);
|
||||
} catch (e) {}
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ResponseService } from "@/ee/schedules/services/response/response.service";
|
||||
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
||||
import { DeleteScheduleOutput } from "@/ee/schedules/outputs/delete-schedule.output";
|
||||
import { GetDefaultScheduleOutput } from "@/ee/schedules/outputs/get-default-schedule.output";
|
||||
import { GetScheduleOutput } from "@/ee/schedules/outputs/get-schedule.output";
|
||||
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
||||
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { ForAtom } from "@/lib/atoms/decorators/for-atom.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
@@ -18,13 +22,11 @@ import {
|
||||
Patch,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Throttle } from "@nestjs/throttler";
|
||||
|
||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries";
|
||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
||||
import type { UpdateScheduleOutputType } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
||||
|
||||
import { CreateScheduleInput } from "../inputs/create-schedule.input";
|
||||
|
||||
@@ -33,21 +35,18 @@ import { CreateScheduleInput } from "../inputs/create-schedule.input";
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
@DocsTags("Schedules")
|
||||
export class SchedulesController {
|
||||
constructor(
|
||||
private readonly schedulesService: SchedulesService,
|
||||
private readonly schedulesResponseService: ResponseService
|
||||
) {}
|
||||
constructor(private readonly schedulesService: SchedulesService) {}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([SCHEDULE_WRITE])
|
||||
async createSchedule(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() bodySchedule: CreateScheduleInput,
|
||||
@ForAtom() forAtom: boolean
|
||||
): Promise<ApiSuccessResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb>> {
|
||||
@Body() bodySchedule: CreateScheduleInput
|
||||
): Promise<CreateScheduleOutput> {
|
||||
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||
const scheduleFormatted = await this.schedulesResponseService.formatSchedule(forAtom, user, schedule);
|
||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -57,13 +56,11 @@ export class SchedulesController {
|
||||
|
||||
@Get("/default")
|
||||
@Permissions([SCHEDULE_READ])
|
||||
async getDefaultSchedule(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@ForAtom() forAtom: boolean
|
||||
): Promise<ApiResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb | null>> {
|
||||
@ApiResponse({ status: 200, description: "Returns the default schedule", type: GetDefaultScheduleOutput })
|
||||
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetDefaultScheduleOutput | null> {
|
||||
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||
const scheduleFormatted = schedule
|
||||
? await this.schedulesResponseService.formatSchedule(forAtom, user, schedule)
|
||||
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
||||
: null;
|
||||
|
||||
return {
|
||||
@@ -74,13 +71,13 @@ export class SchedulesController {
|
||||
|
||||
@Get("/:scheduleId")
|
||||
@Permissions([SCHEDULE_READ])
|
||||
@Throttle({ default: { limit: 10, ttl: 60000 } }) // allow 10 requests per minute (for :scheduleId)
|
||||
async getSchedule(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("scheduleId") scheduleId: number,
|
||||
@ForAtom() forAtom: boolean
|
||||
): Promise<ApiSuccessResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb>> {
|
||||
@Param("scheduleId") scheduleId: number
|
||||
): Promise<GetScheduleOutput> {
|
||||
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||
const scheduleFormatted = await this.schedulesResponseService.formatSchedule(forAtom, user, schedule);
|
||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -90,12 +87,9 @@ export class SchedulesController {
|
||||
|
||||
@Get("/")
|
||||
@Permissions([SCHEDULE_READ])
|
||||
async getSchedules(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@ForAtom() forAtom: boolean
|
||||
): Promise<ApiSuccessResponse<ScheduleResponse[] | ScheduleWithAvailabilitiesForWeb[]>> {
|
||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput> {
|
||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||
const schedulesFormatted = await this.schedulesResponseService.formatSchedules(forAtom, user, schedules);
|
||||
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -108,12 +102,14 @@ export class SchedulesController {
|
||||
@Permissions([SCHEDULE_WRITE])
|
||||
async updateSchedule(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() bodySchedule: UpdateScheduleInput
|
||||
): Promise<ApiSuccessResponse<UpdateScheduleOutputType>> {
|
||||
const updatedSchedule: UpdateScheduleOutputType = await updateScheduleHandler({
|
||||
input: bodySchedule,
|
||||
ctx: { user },
|
||||
});
|
||||
@Body() bodySchedule: UpdateScheduleInput,
|
||||
@Param("scheduleId") scheduleId: string
|
||||
): Promise<UpdateScheduleOutput> {
|
||||
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
||||
user,
|
||||
Number(scheduleId),
|
||||
bodySchedule
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -127,7 +123,7 @@ export class SchedulesController {
|
||||
async deleteSchedule(
|
||||
@GetUser("id") userId: number,
|
||||
@Param("scheduleId") scheduleId: number
|
||||
): Promise<ApiResponse> {
|
||||
): Promise<DeleteScheduleOutput> {
|
||||
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
||||
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,5 @@ export class CreateScheduleInput {
|
||||
availabilities?: CreateAvailabilityInput[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isDefault = true;
|
||||
isDefault!: boolean;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, IsString, IsTimeZone, ValidateNested } from "class-validator";
|
||||
|
||||
export class UpdateScheduleInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsTimeZone()
|
||||
@IsOptional()
|
||||
timeZone?: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateAvailabilityInput)
|
||||
@IsOptional()
|
||||
availabilities?: CreateAvailabilityInput[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class CreateScheduleOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: ScheduleOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => ScheduleOutput)
|
||||
data!: ScheduleOutput;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class DeleteScheduleOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetDefaultScheduleOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: ScheduleOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => ScheduleOutput)
|
||||
data!: ScheduleOutput | null;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetScheduleOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: ScheduleOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => ScheduleOutput)
|
||||
data!: ScheduleOutput;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetSchedulesOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: ScheduleOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ScheduleOutput)
|
||||
@IsArray()
|
||||
data!: ScheduleOutput[];
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Type } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
||||
|
||||
class EventTypeModel {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
eventName?: string | null;
|
||||
}
|
||||
|
||||
class AvailabilityModel {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
userId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
scheduleId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
eventTypeId?: number | null;
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
days!: number[];
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
startTime?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
endTime?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
date?: Date | null;
|
||||
}
|
||||
|
||||
class ScheduleModel {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
userId!: number;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timeZone?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeModel)
|
||||
@IsArray()
|
||||
eventType?: EventTypeModel[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AvailabilityModel)
|
||||
@IsArray()
|
||||
availability?: AvailabilityModel[];
|
||||
}
|
||||
|
||||
export class UpdatedScheduleOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => ScheduleModel)
|
||||
schedule!: ScheduleModel;
|
||||
|
||||
@IsBoolean()
|
||||
isDefault!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timeZone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
prevDefaultId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
currentDefaultId?: number | null;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Type } from "class-transformer";
|
||||
import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
class AvailabilityModel {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
userId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
eventTypeId?: number | null;
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
days!: number[];
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
startTime!: Date;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
endTime!: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
date?: Date | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
scheduleId?: number | null;
|
||||
}
|
||||
|
||||
class WorkingHours {
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
days!: number[];
|
||||
|
||||
@IsInt()
|
||||
startTime!: number;
|
||||
|
||||
@IsInt()
|
||||
endTime!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
userId?: number | null;
|
||||
}
|
||||
|
||||
class TimeRange {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
userId?: number | null;
|
||||
|
||||
@IsDate()
|
||||
start!: Date;
|
||||
|
||||
@IsDate()
|
||||
end!: Date;
|
||||
}
|
||||
|
||||
export class ScheduleOutput {
|
||||
@IsInt()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsBoolean()
|
||||
isManaged!: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkingHours)
|
||||
workingHours!: WorkingHours[];
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AvailabilityModel)
|
||||
@IsArray()
|
||||
schedule!: AvailabilityModel[];
|
||||
|
||||
availability!: TimeRange[][];
|
||||
|
||||
@IsString()
|
||||
timeZone!: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
// note(Lauris) it should be
|
||||
// dateOverrides!: { ranges: TimeRange[] }[];
|
||||
// but docs aren't generating correctly it results in array of strings
|
||||
dateOverrides!: unknown[];
|
||||
|
||||
@IsBoolean()
|
||||
isDefault!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
isLastSchedule!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
readOnly!: boolean;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { UpdatedScheduleOutput } from "@/ee/schedules/outputs/schedule-updated.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UpdateScheduleOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: UpdatedScheduleOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => UpdatedScheduleOutput)
|
||||
data!: UpdatedScheduleOutput;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { ResponseService } from "@/ee/schedules/services/response/response.service";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -10,8 +9,8 @@ import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
||||
providers: [SchedulesRepository, SchedulesService, ResponseService],
|
||||
providers: [SchedulesRepository, SchedulesService],
|
||||
controllers: [SchedulesController],
|
||||
exports: [SchedulesService],
|
||||
exports: [SchedulesService, SchedulesRepository],
|
||||
})
|
||||
export class SchedulesModule {}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||
import { UpdateScheduleInput } from "@/ee/schedules/inputs/update-schedule.input";
|
||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
@@ -78,51 +77,6 @@ export class SchedulesRepository {
|
||||
return schedules;
|
||||
}
|
||||
|
||||
async updateScheduleWithAvailabilities(scheduleId: number, schedule: UpdateScheduleInput) {
|
||||
const existingSchedule = await this.dbRead.prisma.schedule.findUnique({
|
||||
where: { id: scheduleId },
|
||||
});
|
||||
|
||||
if (!existingSchedule) {
|
||||
throw new BadRequestException(`Schedule with ID=${scheduleId} not found`);
|
||||
}
|
||||
|
||||
const updatedScheduleData: Prisma.ScheduleUpdateInput = {
|
||||
user: {
|
||||
connect: {
|
||||
id: existingSchedule.userId,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (schedule.name) updatedScheduleData.name = schedule.name;
|
||||
if (schedule.timeZone) updatedScheduleData.timeZone = schedule.timeZone;
|
||||
|
||||
if (schedule.availabilities && schedule.availabilities.length > 0) {
|
||||
await this.dbWrite.prisma.availability.deleteMany({
|
||||
where: { scheduleId },
|
||||
});
|
||||
|
||||
updatedScheduleData.availability = {
|
||||
createMany: {
|
||||
data: schedule.availabilities.map((availability) => ({
|
||||
...availability,
|
||||
userId: existingSchedule.userId,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const updatedSchedule = await this.dbWrite.prisma.schedule.update({
|
||||
where: { id: scheduleId },
|
||||
data: updatedScheduleData,
|
||||
include: {
|
||||
availability: true,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedSchedule;
|
||||
}
|
||||
|
||||
async deleteScheduleById(scheduleId: number) {
|
||||
return this.dbWrite.prisma.schedule.delete({
|
||||
where: {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import {
|
||||
transformWorkingHoursForClient,
|
||||
transformAvailabilityForClient,
|
||||
transformDateOverridesForClient,
|
||||
} from "@calcom/platform-libraries";
|
||||
import type {
|
||||
ScheduleWithAvailabilities,
|
||||
ScheduleWithAvailabilitiesForWeb,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { ScheduleResponse, schemaScheduleResponse } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class ResponseService {
|
||||
constructor(private readonly schedulesRepository: SchedulesRepository) {}
|
||||
|
||||
async formatSchedule(
|
||||
forAtom: boolean,
|
||||
user: User,
|
||||
schedule: ScheduleWithAvailabilities
|
||||
): Promise<ScheduleResponse | ScheduleWithAvailabilitiesForWeb> {
|
||||
if (forAtom) {
|
||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
|
||||
}
|
||||
|
||||
return schemaScheduleResponse.parse(schedule);
|
||||
}
|
||||
|
||||
async formatSchedules(
|
||||
forAtom: boolean,
|
||||
user: User,
|
||||
schedules: ScheduleWithAvailabilities[]
|
||||
): Promise<ScheduleResponse[] | ScheduleWithAvailabilitiesForWeb[]> {
|
||||
if (forAtom) {
|
||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||
return Promise.all(
|
||||
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
|
||||
);
|
||||
}
|
||||
return schedules.map((schedule) => schemaScheduleResponse.parse(schedule));
|
||||
}
|
||||
|
||||
async transformScheduleForAtom(
|
||||
schedule: ScheduleWithAvailabilities,
|
||||
userSchedulesCount: number,
|
||||
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
|
||||
): Promise<ScheduleWithAvailabilitiesForWeb> {
|
||||
const timeZone = schedule.timeZone || user.timeZone;
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
name: schedule.name,
|
||||
isManaged: schedule.userId !== user.id,
|
||||
workingHours: transformWorkingHoursForClient(schedule),
|
||||
schedule: schedule.availability,
|
||||
availability: transformAvailabilityForClient(schedule),
|
||||
timeZone,
|
||||
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
|
||||
isDefault: user.defaultScheduleId === schedule.id,
|
||||
isLastSchedule: userSchedulesCount <= 1,
|
||||
readOnly: schedule.userId !== user.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||
import { UpdateScheduleInput } from "@/ee/schedules/inputs/update-schedule.input";
|
||||
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Schedule } from "@prisma/client";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries";
|
||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
||||
import {
|
||||
transformWorkingHoursForClient,
|
||||
transformAvailabilityForClient,
|
||||
transformDateOverridesForClient,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { UpdateScheduleInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulesService {
|
||||
@@ -14,6 +24,16 @@ export class SchedulesService {
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
async createUserDefaultSchedule(userId: number, timeZone: string) {
|
||||
const schedule = {
|
||||
isDefault: true,
|
||||
name: "Default schedule",
|
||||
timeZone,
|
||||
};
|
||||
|
||||
return this.createUserSchedule(userId, schedule);
|
||||
}
|
||||
|
||||
async createUserSchedule(userId: number, schedule: CreateScheduleInput) {
|
||||
const availabilities = schedule.availabilities?.length
|
||||
? schedule.availabilities
|
||||
@@ -56,25 +76,31 @@ export class SchedulesService {
|
||||
return this.schedulesRepository.getSchedulesByUserId(userId);
|
||||
}
|
||||
|
||||
async updateUserSchedule(userId: number, scheduleId: number, schedule: UpdateScheduleInput) {
|
||||
async updateUserSchedule(user: UserWithProfile, scheduleId: number, bodySchedule: UpdateScheduleInput) {
|
||||
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||
|
||||
if (!existingSchedule) {
|
||||
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||
}
|
||||
|
||||
this.checkUserOwnsSchedule(userId, existingSchedule);
|
||||
this.checkUserOwnsSchedule(user.id, existingSchedule);
|
||||
|
||||
const updatedSchedule = await this.schedulesRepository.updateScheduleWithAvailabilities(
|
||||
scheduleId,
|
||||
schedule
|
||||
);
|
||||
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
|
||||
const scheduleFormatted = await this.formatScheduleForAtom(user, schedule);
|
||||
|
||||
if (schedule.isDefault) {
|
||||
await this.usersRepository.setDefaultSchedule(userId, updatedSchedule.id);
|
||||
if (!bodySchedule.schedule) {
|
||||
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
|
||||
// the schedule is sent and then passed to the update handler. Notably, availability is passed too
|
||||
// and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect
|
||||
// schedule that would be passed by the web app. If we don't, then updating schedule name will erase
|
||||
// schedule.
|
||||
bodySchedule.schedule = scheduleFormatted.availability;
|
||||
}
|
||||
|
||||
return updatedSchedule;
|
||||
return updateScheduleHandler({
|
||||
input: { scheduleId: Number(scheduleId), ...bodySchedule },
|
||||
ctx: { user },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserSchedule(userId: number, scheduleId: number) {
|
||||
@@ -89,6 +115,44 @@ export class SchedulesService {
|
||||
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
async formatScheduleForAtom(user: User, schedule: ScheduleWithAvailabilities): Promise<ScheduleOutput> {
|
||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
|
||||
}
|
||||
|
||||
async formatSchedulesForAtom(
|
||||
user: User,
|
||||
schedules: ScheduleWithAvailabilities[]
|
||||
): Promise<ScheduleOutput[]> {
|
||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||
return Promise.all(
|
||||
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
|
||||
);
|
||||
}
|
||||
|
||||
async transformScheduleForAtom(
|
||||
schedule: ScheduleWithAvailabilities,
|
||||
userSchedulesCount: number,
|
||||
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
|
||||
): Promise<ScheduleOutput> {
|
||||
const timeZone = schedule.timeZone || user.timeZone;
|
||||
const defaultSchedule = await this.getUserScheduleDefault(user.id);
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
name: schedule.name,
|
||||
isManaged: schedule.userId !== user.id,
|
||||
workingHours: transformWorkingHoursForClient(schedule),
|
||||
schedule: schedule.availability,
|
||||
availability: transformAvailabilityForClient(schedule),
|
||||
timeZone,
|
||||
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
|
||||
isDefault: defaultSchedule?.id === schedule.id,
|
||||
isLastSchedule: userSchedulesCount <= 1,
|
||||
readOnly: schedule.userId !== user.id,
|
||||
};
|
||||
}
|
||||
|
||||
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
||||
if (userId !== schedule.userId) {
|
||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||
|
||||
@@ -12,6 +12,9 @@ export type Environment = {
|
||||
SENTRY_DSN: string;
|
||||
LOG_LEVEL: keyof typeof logLevels;
|
||||
REDIS_URL: string;
|
||||
STRIPE_API_KEY: string;
|
||||
STRIPE_WEBHOOK_SECRET: string;
|
||||
WEB_APP_URL: string;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -10,12 +11,19 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const statusCode = exception.getStatus();
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
PrismaClientUnknownRequestError,
|
||||
PrismaClientValidationError,
|
||||
} from "@prisma/client/runtime/library";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -31,10 +32,16 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
catch(error: PrismaError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`PrismaError: ${error.message}`, {
|
||||
error,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||
status: ERROR_STATUS,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
|
||||
import { BaseExceptionFilter } from "@nestjs/core";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -12,11 +13,16 @@ export class SentryFilter extends BaseExceptionFilter {
|
||||
handleUnknownError(exception: any, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
|
||||
// capture if client has been init
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
@@ -11,7 +12,8 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: TRPCError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let statusCode = 500;
|
||||
switch (exception.code) {
|
||||
case "UNAUTHORIZED":
|
||||
@@ -38,10 +40,18 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
statusCode = 500;
|
||||
break;
|
||||
}
|
||||
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { BAD_REQUEST, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
@@ -12,11 +13,18 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
||||
catch(error: ZodError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`ZodError: ${error.message}`, {
|
||||
error,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
|
||||
response.status(HttpStatus.BAD_REQUEST).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||
|
||||
export const ForAtom = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
|
||||
export const ForAtom = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.query.for === "atom";
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user