Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b96786125c | ||
|
|
9451f38cec | ||
|
|
306f53dec7 | ||
|
|
db865a680f | ||
|
|
aca552c9f7 | ||
|
|
b202c4e79f | ||
|
|
972ce6911a | ||
|
|
751530b460 | ||
|
|
3b263e9282 | ||
|
|
2cbf9e1ac8 | ||
|
|
6283535d6a | ||
|
|
a03ae81509 | ||
|
|
a7e05aa016 | ||
|
|
f8cbf8b1c4 | ||
|
|
ccc294e567 | ||
|
|
0be344b1ba | ||
|
|
7a525e476a | ||
|
|
fe62f9c6de | ||
|
|
9608f6518f | ||
|
|
2f8fa20ec6 | ||
|
|
6f097b0a74 | ||
|
|
159bbb53b6 | ||
|
|
622160ea17 | ||
|
|
5d543c864f | ||
|
|
ed745623eb | ||
|
|
29bca76e91 | ||
|
|
33d0fee43e | ||
|
|
bb13de4176 | ||
|
|
c854dd092c | ||
|
|
26d64bd93f | ||
|
|
1025238eee | ||
|
|
eec76ecfe2 | ||
|
|
f5876ae6d6 | ||
|
|
ccc8cdc1ff | ||
|
|
a1840c9362 | ||
|
|
22212e7edb | ||
|
|
6ffe7005dd | ||
|
|
4890d72e30 | ||
|
|
a531541273 | ||
|
|
2bced59b47 | ||
|
|
e9554604e7 | ||
|
|
48c51da853 | ||
|
|
cc2164657c | ||
|
|
a35683667b | ||
|
|
cf403bebf1 | ||
|
|
f3e4bbc753 | ||
|
|
4bfbac6649 | ||
|
|
3d7fd7eb01 | ||
|
|
ec42900e25 | ||
|
|
032b243248 | ||
|
|
b70b5a5449 | ||
|
|
cc5dfb06bd |
No files matched your search
+10
-2
@@ -169,8 +169,7 @@ STRIPE_WEBHOOK_SECRET=
|
|||||||
STRIPE_WEBHOOK_SECRET_APPS=
|
STRIPE_WEBHOOK_SECRET_APPS=
|
||||||
STRIPE_PRIVATE_KEY=
|
STRIPE_PRIVATE_KEY=
|
||||||
STRIPE_CLIENT_ID=
|
STRIPE_CLIENT_ID=
|
||||||
PAYMENT_FEE_FIXED=
|
|
||||||
PAYMENT_FEE_PERCENTAGE=
|
|
||||||
|
|
||||||
# Use for internal Public API Keys and optional
|
# Use for internal Public API Keys and optional
|
||||||
API_KEY_PREFIX=cal_
|
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
|
# Set the following value to true if you wish to enable Team Impersonation
|
||||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||||
|
|
||||||
@@ -346,3 +349,8 @@ NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
|||||||
|
|
||||||
# Ratelimiting via unkey
|
# Ratelimiting via unkey
|
||||||
UNKEY_ROOT_KEY=
|
UNKEY_ROOT_KEY=
|
||||||
|
|
||||||
|
|
||||||
|
# Used for Cal.ai Enterprise Voice AI Agents
|
||||||
|
# https://retellai.com
|
||||||
|
RETELL_AI_KEY=
|
||||||
@@ -4,3 +4,4 @@ node_modules
|
|||||||
**/**/public
|
**/**/public
|
||||||
packages/prisma/zod
|
packages/prisma/zod
|
||||||
apps/web/public/embed
|
apps/web/public/embed
|
||||||
|
packages/ui/components/icon/dynamicIconImports.tsx
|
||||||
@@ -3,7 +3,7 @@ description: "Cache or restore if necessary"
|
|||||||
inputs:
|
inputs:
|
||||||
DATABASE_URL:
|
DATABASE_URL:
|
||||||
required: false
|
required: false
|
||||||
default: "postgresql://postgres:@localhost:5432/calendso"
|
default: "postgresql://postgres:postgres@localhost:5432/calendso"
|
||||||
path:
|
path:
|
||||||
required: false
|
required: false
|
||||||
default: "backups/backup.sql"
|
default: "backups/backup.sql"
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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 }}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -10,13 +10,19 @@ jobs:
|
|||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13.13
|
image: postgres:13
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
mailhog:
|
mailhog:
|
||||||
|
|||||||
@@ -10,13 +10,19 @@ jobs:
|
|||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13.13
|
image: postgres:13
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -10,13 +10,19 @@ jobs:
|
|||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13.13
|
image: postgres:13
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
mailhog:
|
mailhog:
|
||||||
|
|||||||
@@ -10,13 +10,19 @@ jobs:
|
|||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13.13
|
image: postgres:13
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
mailhog:
|
mailhog:
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ jobs:
|
|||||||
uses: ./.github/workflows/production-build.yml
|
uses: ./.github/workflows/production-build.yml
|
||||||
secrets: inherit
|
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:
|
build-without-database:
|
||||||
name: Production build (without database)
|
name: Production build (without database)
|
||||||
needs: [changes]
|
needs: [changes]
|
||||||
@@ -99,7 +106,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
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()
|
if: always()
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ env:
|
|||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
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 }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ env:
|
|||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
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 }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -42,13 +44,19 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13.13
|
image: postgres:13
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ packages/prisma/zod
|
|||||||
packages/prisma/enums
|
packages/prisma/enums
|
||||||
apps/web/public/embed
|
apps/web/public/embed
|
||||||
apps/api/v2/swagger/documentation.json
|
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.
|
- 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.
|
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Priorities
|
## Priorities
|
||||||
|
|
||||||
<table>
|
<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:
|
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`:
|
If your last commit has the `yarn.lock` file alongside other files and you only wish to uncommit the `yarn.lock`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout HEAD~1 yarn.lock
|
git checkout HEAD~1 yarn.lock
|
||||||
git commit -m "Revert yarn.lock changes"
|
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`:
|
If you've pushed the commit with the `yarn.lock`:
|
||||||
|
|
||||||
1. Correct the commit locally using the above method.
|
1. Correct the commit locally using the above method.
|
||||||
2. Carefully force push:
|
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**:
|
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:
|
- Find the commit hash before the `yarn.lock` was unintentionally committed. You can do this by viewing the Git log:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git log yarn.lock
|
git log yarn.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
- Once you have identified the commit hash, use it to checkout the previous version of `yarn.lock`:
|
- Once you have identified the commit hash, use it to checkout the previous version of `yarn.lock`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout <commit_hash> yarn.lock
|
git checkout <commit_hash> yarn.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Commit the Reverted Version**:
|
2. **Commit the Reverted Version**:
|
||||||
- After checking out the previous version of the `yarn.lock`, commit this change:
|
- After checking out the previous version of the `yarn.lock`, commit this change:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git commit -m "Revert yarn.lock to its state before unintended changes"
|
git commit -m "Revert yarn.lock to its state before unintended changes"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Proceed with Caution**:
|
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:
|
- 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
|
```bash
|
||||||
git pull origin <your-branch-name>
|
git pull origin <your-branch-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
- Then push the updated branch:
|
- Then push the updated branch:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push origin <your-branch-name>
|
git push origin <your-branch-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Lastly, make sure to keep the branches updated (e.g. click the `Update branch` button on GitHub PR).
|
||||||
@@ -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.
|
Here is what you need to be able to run Cal.com.
|
||||||
|
|
||||||
- Node.js (Version: >=18.x)
|
- Node.js (Version: >=18.x)
|
||||||
- PostgreSQL
|
- PostgreSQL (Version: >=13.x)
|
||||||
- Yarn _(recommended)_
|
- 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).
|
> 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
|
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>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ test.describe("Org", () => {
|
|||||||
];
|
];
|
||||||
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
||||||
expect(response?.status()).toBe(200);
|
expect(response?.status()).toBe(200);
|
||||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
|
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Group Meeting");
|
||||||
|
|
||||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
||||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
|||||||
|
|
||||||
# Get it in console.cal.com
|
# Get it in console.cal.com
|
||||||
CALCOM_LICENSE_KEY=""
|
CALCOM_LICENSE_KEY=""
|
||||||
|
NEXT_PUBLIC_API_V2_ROOT_URL=http://localhost:5555
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { NextMiddleware } from "next-api-middleware";
|
import type { NextMiddleware } from "next-api-middleware";
|
||||||
|
|
||||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit";
|
|
||||||
|
|
||||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
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,
|
identifier: req.query.apiKey as string,
|
||||||
rateLimitingType: "api",
|
rateLimitingType: "api",
|
||||||
onRateLimiterResponse: (response) => {
|
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-Remaining", response.remaining);
|
||||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,22 @@ const nextConfig = {
|
|||||||
source: "/v:version/:rest*",
|
source: "/v:version/:rest*",
|
||||||
destination: "/api/v:version/:rest*",
|
destination: "/api/v:version/:rest*",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/api/v2",
|
||||||
|
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/api/v2/health",
|
||||||
|
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/api/v2/docs/:path*",
|
||||||
|
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/docs/:path*`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/api/v2/:path*",
|
||||||
|
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/api/v2/:path*`,
|
||||||
|
},
|
||||||
// This redirects requests to api/v*/ to /api/ passing version as a query parameter.
|
// This redirects requests to api/v*/ to /api/ passing version as a query parameter.
|
||||||
{
|
{
|
||||||
source: "/api/v:version/:rest*",
|
source: "/api/v:version/:rest*",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
|||||||
await canAccessTeamEventOrThrow(req, {
|
await canAccessTeamEventOrThrow(req, {
|
||||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if (eventType?.userId === req.userId) return true; // is owner.
|
if (eventType?.userId === req.userId) return true; // is owner.
|
||||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import type { NextApiRequest } from "next";
|
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 { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
@@ -105,10 +105,11 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
};
|
};
|
||||||
delete data.slug;
|
delete data.slug;
|
||||||
if (IS_TEAM_BILLING_ENABLED) {
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
const checkoutSession = await purchaseTeamSubscription({
|
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||||
teamId: _team.id,
|
teamId: _team.id,
|
||||||
seats: _team.members.length,
|
seats: _team.members.length,
|
||||||
userId,
|
userId,
|
||||||
|
pricePerSeat: null,
|
||||||
});
|
});
|
||||||
if (!checkoutSession.url)
|
if (!checkoutSession.url)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
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 { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
@@ -197,8 +198,9 @@ const generateTeamCheckoutSession = async ({
|
|||||||
customer_update: {
|
customer_update: {
|
||||||
address: "auto",
|
address: "auto",
|
||||||
},
|
},
|
||||||
|
// Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
|
||||||
automatic_tax: {
|
automatic_tax: {
|
||||||
enabled: true,
|
enabled: IS_PRODUCTION,
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
pendingPaymentTeamId,
|
pendingPaymentTeamId,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["*"],
|
"~/*": ["*"],
|
||||||
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||||
}
|
},
|
||||||
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ LOG_LEVEL=
|
|||||||
NEXTAUTH_SECRET=
|
NEXTAUTH_SECRET=
|
||||||
DATABASE_URL=
|
DATABASE_URL=
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
SENTRY_DNS=
|
SENTRY_DSN=
|
||||||
|
|
||||||
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
|
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "@nestjs/swagger",
|
"name": "@nestjs/swagger",
|
||||||
"options": { "dtoFileNameSuffix": ".input.ts", "classValidatorShim": true }
|
"options": { "dtoFileNameSuffix": [".input.ts", ".output.ts", ".dto.ts"], "classValidatorShim": true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class AppLoggerMiddleware implements NestMiddleware {
|
|||||||
private logger = new Logger("HTTP");
|
private logger = new Logger("HTTP");
|
||||||
|
|
||||||
use(request: Request, response: Response, next: NextFunction): void {
|
use(request: Request, response: Response, next: NextFunction): void {
|
||||||
const { ip, method, protocol, originalUrl, path: url } = request;
|
const { ip, method, originalUrl } = request;
|
||||||
const userAgent = request.get("user-agent") || "";
|
const userAgent = request.get("user-agent") || "";
|
||||||
|
|
||||||
response.on("close", () => {
|
response.on("close", () => {
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (process.env.SENTRY_DNS) {
|
if (process.env.SENTRY_DSN) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: getEnv("SENTRY_DNS"),
|
dsn: getEnv("SENTRY_DSN"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ const loadConfig = (): AppConfig => {
|
|||||||
port: Number(getEnv("API_PORT", "5555")),
|
port: Number(getEnv("API_PORT", "5555")),
|
||||||
path: getEnv("API_URL", "http://localhost"),
|
path: getEnv("API_URL", "http://localhost"),
|
||||||
url: `${getEnv("API_URL", "http://localhost")}${
|
url: `${getEnv("API_URL", "http://localhost")}${
|
||||||
process.env.API_PORT ? `:${Number(getEnv("API_PORT", "5555"))}` : ""
|
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
|
||||||
|
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||||
|
: ""
|
||||||
}/api/v2`,
|
}/api/v2`,
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
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 { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.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 { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
describe("Bookings Endpoints", () => {
|
||||||
|
describe("User Authenticated", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
|
|
||||||
|
const userEmail = "bookings-controller-e2e@api.com";
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withAccessTokenAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
|
||||||
|
providers: [SchedulesRepository, SchedulesService],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 bookingLanguage = "en";
|
||||||
|
const bookingHashedLink = "";
|
||||||
|
const bookingMetadata = {};
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
start: bookingStart,
|
||||||
|
end: bookingEnd,
|
||||||
|
eventTypeId: bookingEventTypeId,
|
||||||
|
timeZone: bookingTimeZone,
|
||||||
|
language: bookingLanguage,
|
||||||
|
metadata: bookingMetadata,
|
||||||
|
hashedLink: bookingHashedLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/ee/bookings")
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
||||||
|
response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.user.email).toBeDefined();
|
||||||
|
expect(responseBody.data.user.email).toEqual(userEmail);
|
||||||
|
expect(responseBody.data.id).toBeDefined();
|
||||||
|
expect(responseBody.data.uid).toBeDefined();
|
||||||
|
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||||
|
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||||
|
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||||
|
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||||
|
|
||||||
|
createdBooking = responseBody.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get bookings", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get("/api/v2/ee/bookings")
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getAllUserBookings>>> =
|
||||||
|
response.body;
|
||||||
|
const fetchedBooking = responseBody.data.bookings[0];
|
||||||
|
|
||||||
|
expect(responseBody.data.bookings.length).toEqual(1);
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(fetchedBooking).toBeDefined();
|
||||||
|
|
||||||
|
expect(fetchedBooking.id).toEqual(createdBooking.id);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get booking", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>> = response.body;
|
||||||
|
const { bookingInfo } = responseBody.data;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(bookingInfo?.id).toBeDefined();
|
||||||
|
expect(bookingInfo?.uid).toBeDefined();
|
||||||
|
expect(bookingInfo?.id).toEqual(createdBooking.id);
|
||||||
|
expect(bookingInfo?.uid).toEqual(createdBooking.uid);
|
||||||
|
expect(bookingInfo?.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||||
|
expect(bookingInfo?.startTime).toEqual(createdBooking.startTime);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual("instant");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cancel a booking", async () => {
|
||||||
|
const bookingId = createdBooking.id;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
allRemainingBookings: false,
|
||||||
|
cancellationReason: "Was fighting some unforseen rescheduling demons",
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: ApiResponse<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> =
|
||||||
|
response.body;
|
||||||
|
|
||||||
|
expect(bookingId).toBeDefined();
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
@@ -11,12 +14,23 @@ import {
|
|||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
Body,
|
Body,
|
||||||
HttpException,
|
HttpException,
|
||||||
|
Param,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
NotFoundException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { NextApiRequest } from "next/types";
|
import { NextApiRequest } from "next/types";
|
||||||
|
|
||||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
getAllUserBookings,
|
||||||
|
getBookingInfo,
|
||||||
|
handleCancelBooking,
|
||||||
|
getBookingForReschedule,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
import {
|
import {
|
||||||
handleNewBooking,
|
handleNewBooking,
|
||||||
BookingResponse,
|
BookingResponse,
|
||||||
@@ -24,7 +38,9 @@ import {
|
|||||||
handleNewRecurringBooking,
|
handleNewRecurringBooking,
|
||||||
handleInstantMeeting,
|
handleInstantMeeting,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
|
import { GetBookingsInput, CancelBookingInput } from "@calcom/platform-types";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "ee/bookings",
|
path: "ee/bookings",
|
||||||
@@ -34,7 +50,65 @@ import { ApiResponse } from "@calcom/platform-types";
|
|||||||
export class BookingsController {
|
export class BookingsController {
|
||||||
private readonly logger = new Logger("ee bookings controller");
|
private readonly logger = new Logger("ee bookings controller");
|
||||||
|
|
||||||
constructor(private readonly oAuthFlowService: OAuthFlowService) {}
|
constructor(
|
||||||
|
private readonly oAuthFlowService: OAuthFlowService,
|
||||||
|
private readonly prismaReadService: PrismaReadService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// note(Rajiv): currently this endpoint is atoms only
|
||||||
|
@Get("/")
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
async getBookings(
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Query() queryParams: GetBookingsInput
|
||||||
|
): Promise<ApiResponse<unknown>> {
|
||||||
|
const { filters, cursor, limit } = queryParams;
|
||||||
|
const bookings = await getAllUserBookings({
|
||||||
|
bookingListingByStatus: filters.status,
|
||||||
|
skip: cursor ?? 0,
|
||||||
|
take: limit ?? 10,
|
||||||
|
filters,
|
||||||
|
ctx: {
|
||||||
|
user: { email: user.email, id: user.id },
|
||||||
|
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: bookings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// note(Rajiv): currently this endpoint is atoms only
|
||||||
|
@Get("/:bookingUid")
|
||||||
|
async getBooking(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||||
|
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||||
|
|
||||||
|
if (!bookingInfo) {
|
||||||
|
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: bookingInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: booking,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([BOOKING_WRITE])
|
@Permissions([BOOKING_WRITE])
|
||||||
@@ -56,6 +130,30 @@ export class BookingsController {
|
|||||||
throw new InternalServerErrorException("Could not create booking.");
|
throw new InternalServerErrorException("Could not create booking.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("/:bookingId/cancel")
|
||||||
|
@Permissions([BOOKING_WRITE])
|
||||||
|
async cancelBooking(
|
||||||
|
@Req() req: Request & { userId?: number },
|
||||||
|
@Param("bookingId") bookingId: string,
|
||||||
|
@Body() body: CancelBookingInput
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
if (bookingId) {
|
||||||
|
req.userId = await this.getOwnerId(req);
|
||||||
|
req.body = { ...body, id: parseInt(bookingId) };
|
||||||
|
try {
|
||||||
|
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
handleBookingErrors(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new NotFoundException("Booking ID is required.");
|
||||||
|
}
|
||||||
|
throw new InternalServerErrorException("Could not cancel booking.");
|
||||||
|
}
|
||||||
|
|
||||||
@Post("/reccuring")
|
@Post("/reccuring")
|
||||||
@Permissions([BOOKING_WRITE])
|
@Permissions([BOOKING_WRITE])
|
||||||
async createReccuringBooking(
|
async createReccuringBooking(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||||
import { IsBoolean, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
|
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||||
|
|
||||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
|||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
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 { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
||||||
@@ -16,8 +16,6 @@ import { EventBusyDate } from "@calcom/types/Calendar";
|
|||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
export class CalendarsController {
|
export class CalendarsController {
|
||||||
private readonly logger = new Logger("ee overlay calendars controller");
|
|
||||||
|
|
||||||
constructor(private readonly calendarsService: CalendarsService) {}
|
constructor(private readonly calendarsService: CalendarsService) {}
|
||||||
|
|
||||||
@Get("/busy-times")
|
@Get("/busy-times")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { EventTypesModule } from "@/ee/event-types/event-types.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 { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
@@ -61,7 +63,7 @@ describe("Event types Endpoints", () => {
|
|||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
const userEmail = "event-types-test-e2e@api.com";
|
const userEmail = "event-types-test-e2e@api.com";
|
||||||
const name = "bob";
|
const name = "bob the builder";
|
||||||
const username = name;
|
const username = name;
|
||||||
let eventType: EventType;
|
let eventType: EventType;
|
||||||
let user: User;
|
let user: User;
|
||||||
@@ -86,6 +88,7 @@ describe("Event types Endpoints", () => {
|
|||||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
@@ -95,17 +98,6 @@ describe("Event types Endpoints", () => {
|
|||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
|
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
|
||||||
|
|
||||||
eventType = await eventTypesRepositoryFixture.create(
|
|
||||||
{
|
|
||||||
length: 60,
|
|
||||||
title: "peer coding session",
|
|
||||||
slug: "peer-coding",
|
|
||||||
},
|
|
||||||
user.id
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +121,50 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(user).toBeDefined();
|
expect(user).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create an event type", async () => {
|
||||||
|
const body: CreateEventTypeInput = {
|
||||||
|
title: "Test Event Type",
|
||||||
|
slug: "test-event-type",
|
||||||
|
description: "A description of the test event type.",
|
||||||
|
length: 60,
|
||||||
|
hidden: false,
|
||||||
|
locations: [
|
||||||
|
{
|
||||||
|
type: "Online",
|
||||||
|
link: "https://example.com/meet",
|
||||||
|
displayLocationPublicly: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventType> = response.body;
|
||||||
|
expect(responseBody.data).toHaveProperty("id");
|
||||||
|
expect(responseBody.data.title).toEqual(body.title);
|
||||||
|
eventType = responseBody.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update event type", async () => {
|
||||||
|
const newTitle = "Updated title";
|
||||||
|
|
||||||
|
const body: UpdateEventTypeInput = {
|
||||||
|
title: newTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async () => {
|
||||||
|
eventType.title = newTitle;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it(`/GET/:id`, async () => {
|
it(`/GET/:id`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/${eventType.id}`)
|
.get(`/api/v2/event-types/${eventType.id}`)
|
||||||
@@ -154,10 +190,8 @@ describe("Event types Endpoints", () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypesByViewer> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypesByViewer> = response.body;
|
||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
console.log("asap responseBody.data", responseBody.data);
|
|
||||||
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
||||||
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
||||||
expect(responseBody.data.eventTypeGroups[0]).toBeDefined();
|
expect(responseBody.data.eventTypeGroups[0]).toBeDefined();
|
||||||
@@ -178,7 +212,6 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
console.log("asap responseBody.data", responseBody.data);
|
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.length).toEqual(1);
|
expect(responseBody.data.length).toEqual(1);
|
||||||
expect(responseBody.data[0].id).toEqual(eventType.id);
|
expect(responseBody.data[0].id).toEqual(eventType.id);
|
||||||
@@ -192,10 +225,18 @@ describe("Event types Endpoints", () => {
|
|||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should delete schedule", async () => {
|
||||||
|
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
await eventTypesRepositoryFixture.delete(eventType.id);
|
try {
|
||||||
|
await eventTypesRepositoryFixture.delete(eventType.id);
|
||||||
|
} catch (e) {
|
||||||
|
// Event type might have been deleted by the test
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(user.id);
|
await userRepositoryFixture.delete(user.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,25 +1,41 @@
|
|||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.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 { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||||
|
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||||
|
import { 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 { 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 { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Controller, UseGuards, Get, Param, Post, Body, NotFoundException } from "@nestjs/common";
|
import {
|
||||||
import { EventType } from "@prisma/client";
|
Controller,
|
||||||
|
UseGuards,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
NotFoundException,
|
||||||
|
Patch,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Delete,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EventTypesByViewer } from "@calcom/lib";
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import type { EventType as AtomEventType, EventTypesPublic } from "@calcom/platform-libraries";
|
|
||||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||||
import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "event-types",
|
path: "event-types",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
|
@DocsTags("Event types")
|
||||||
export class EventTypesController {
|
export class EventTypesController {
|
||||||
constructor(private readonly eventTypesService: EventTypesService) {}
|
constructor(private readonly eventTypesService: EventTypesService) {}
|
||||||
|
|
||||||
@@ -29,8 +45,8 @@ export class EventTypesController {
|
|||||||
async createEventType(
|
async createEventType(
|
||||||
@Body() body: CreateEventTypeInput,
|
@Body() body: CreateEventTypeInput,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<ApiResponse<EventType>> {
|
): Promise<CreateEventTypeOutput> {
|
||||||
const eventType = await this.eventTypesService.createUserEventType(user.id, body);
|
const eventType = await this.eventTypesService.createUserEventType(user, body);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -43,12 +59,9 @@ export class EventTypesController {
|
|||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
async getEventType(
|
async getEventType(
|
||||||
@Param("eventTypeId") eventTypeId: string,
|
@Param("eventTypeId") eventTypeId: string,
|
||||||
@ForAtom() forAtom: boolean,
|
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<ApiSuccessResponse<EventType | AtomEventType>> {
|
): Promise<GetEventTypeOutput> {
|
||||||
const eventType = forAtom
|
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
||||||
? await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId))
|
|
||||||
: await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
|
|
||||||
|
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||||
@@ -63,7 +76,7 @@ export class EventTypesController {
|
|||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([EVENT_TYPE_READ])
|
@Permissions([EVENT_TYPE_READ])
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<ApiSuccessResponse<EventTypesByViewer>> {
|
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
|
||||||
const eventTypes = await getEventTypesByViewer({
|
const eventTypes = await getEventTypesByViewer({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
profile: {
|
profile: {
|
||||||
@@ -79,9 +92,7 @@ export class EventTypesController {
|
|||||||
|
|
||||||
@Get("/:username/public")
|
@Get("/:username/public")
|
||||||
@Permissions([EVENT_TYPE_READ])
|
@Permissions([EVENT_TYPE_READ])
|
||||||
async getPublicEventTypes(
|
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
|
||||||
@Param("username") username: string
|
|
||||||
): Promise<ApiSuccessResponse<EventTypesPublic>> {
|
|
||||||
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -89,4 +100,41 @@ export class EventTypesController {
|
|||||||
data: eventTypes,
|
data: eventTypes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch("/:eventTypeId")
|
||||||
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async updateEventType(
|
||||||
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
|
@Body() body: UpdateEventTypeInput,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<UpdateEventTypeOutput> {
|
||||||
|
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("/:eventTypeId")
|
||||||
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
async deleteEventType(
|
||||||
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
|
@GetUser("id") userId: number
|
||||||
|
): Promise<DeleteEventTypeOutput> {
|
||||||
|
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: {
|
||||||
|
id: eventType.id,
|
||||||
|
length: eventType.length,
|
||||||
|
slug: eventType.slug,
|
||||||
|
title: eventType.title,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,13 @@ import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
|||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule],
|
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
||||||
providers: [EventTypesRepository, EventTypesService],
|
providers: [EventTypesRepository, EventTypesService],
|
||||||
controllers: [EventTypesController],
|
controllers: [EventTypesController],
|
||||||
exports: [EventTypesService, EventTypesRepository],
|
exports: [EventTypesService, EventTypesRepository],
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import { getEventTypeById } from "@calcom/platform-libraries";
|
|||||||
export class EventTypesRepository {
|
export class EventTypesRepository {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async createUserEventType(userId: number, body: CreateEventTypeInput) {
|
async createUserEventType(
|
||||||
|
userId: number,
|
||||||
|
body: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">
|
||||||
|
) {
|
||||||
return this.dbWrite.prisma.eventType.create({
|
return this.dbWrite.prisma.eventType.create({
|
||||||
data: {
|
data: {
|
||||||
...body,
|
...body,
|
||||||
@@ -59,4 +62,19 @@ export class EventTypesRepository {
|
|||||||
async getEventTypeById(eventTypeId: number) {
|
async getEventTypeById(eventTypeId: number) {
|
||||||
return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId } });
|
return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserEventTypeBySlug(userId: number, slug: string) {
|
||||||
|
return this.dbRead.prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_slug: {
|
||||||
|
userId: userId,
|
||||||
|
slug: slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEventType(eventTypeId: number) {
|
||||||
|
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,53 @@
|
|||||||
import { IsNumber, IsString, Min } from "class-validator";
|
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||||
|
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsString, IsNumber, IsBoolean, IsOptional, ValidateNested, Min, IsArray } from "class-validator";
|
||||||
|
|
||||||
|
export const CREATE_EVENT_LENGTH_EXAMPLE = 60;
|
||||||
|
export const CREATE_EVENT_SLUG_EXAMPLE = "cooking-class";
|
||||||
|
export const CREATE_EVENT_TITLE_EXAMPLE = "Learn the secrets of masterchief!";
|
||||||
|
export const CREATE_EVENT_DESCRIPTION_EXAMPLE =
|
||||||
|
"Discover the culinary wonders of the Argentina by making the best flan ever!";
|
||||||
|
|
||||||
|
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||||
|
// Just uncomment any below when requested.
|
||||||
export class CreateEventTypeInput {
|
export class CreateEventTypeInput {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
|
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||||
length!: number;
|
length!: number;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||||
slug!: string;
|
slug!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
hidden?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EventTypeLocation)
|
||||||
|
@IsArray()
|
||||||
|
locations?: EventTypeLocation[];
|
||||||
|
|
||||||
|
// @ApiHideProperty()
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsNumber()
|
||||||
|
// teamId?: number;
|
||||||
|
|
||||||
|
// @ApiHideProperty()
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsEnum(SchedulingType)
|
||||||
|
// schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export enum Editable {
|
||||||
|
system = "system",
|
||||||
|
systemButOptional = "system-but-optional",
|
||||||
|
systemButHidden = "system-but-hidden",
|
||||||
|
user = "user",
|
||||||
|
userReadonly = "user-readonly",
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export enum BaseField {
|
||||||
|
number = "number",
|
||||||
|
boolean = "boolean",
|
||||||
|
address = "address",
|
||||||
|
name = "name",
|
||||||
|
text = "text",
|
||||||
|
textarea = "textarea",
|
||||||
|
email = "email",
|
||||||
|
phone = "phone",
|
||||||
|
multiemail = "multiemail",
|
||||||
|
select = "select",
|
||||||
|
multiselect = "multiselect",
|
||||||
|
checkbox = "checkbox",
|
||||||
|
radio = "radio",
|
||||||
|
radioInput = "radioInput",
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export enum Frequency {
|
||||||
|
YEARLY = 0,
|
||||||
|
MONTHLY = 1,
|
||||||
|
WEEKLY = 2,
|
||||||
|
DAILY = 3,
|
||||||
|
HOURLY = 4,
|
||||||
|
MINUTELY = 5,
|
||||||
|
SECONDLY = 6,
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum PeriodType {
|
||||||
|
UNLIMITED = "UNLIMITED",
|
||||||
|
ROLLING = "ROLLING",
|
||||||
|
RANGE = "RANGE",
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum SchedulingType {
|
||||||
|
ROUND_ROBIN = "ROUND_ROBIN",
|
||||||
|
COLLECTIVE = "COLLECTIVE",
|
||||||
|
MANAGED = "MANAGED",
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||||
|
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
|
||||||
|
|
||||||
|
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||||
|
// Just uncomment any below when requested.
|
||||||
|
|
||||||
|
export class EventTypeLocation {
|
||||||
|
@IsString()
|
||||||
|
@DocsProperty({ example: "link" })
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiHideProperty()
|
||||||
|
address?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUrl()
|
||||||
|
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||||
|
link?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
displayLocationPublicly?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiHideProperty()
|
||||||
|
hostPhoneNumber?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiHideProperty()
|
||||||
|
credentialId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiHideProperty()
|
||||||
|
teamName?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
import { Editable } from "@/ee/event-types/inputs/enums/editable";
|
||||||
|
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
|
||||||
|
import { Frequency } from "@/ee/event-types/inputs/enums/frequency";
|
||||||
|
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsBoolean,
|
||||||
|
IsOptional,
|
||||||
|
ValidateNested,
|
||||||
|
Min,
|
||||||
|
IsInt,
|
||||||
|
IsEnum,
|
||||||
|
IsArray,
|
||||||
|
IsDate,
|
||||||
|
IsNumber,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||||
|
// Just uncomment any below when requested. Go to bottom of file to see UpdateEventTypeInput.
|
||||||
|
|
||||||
|
class Option {
|
||||||
|
@IsString()
|
||||||
|
value!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
label!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Source {
|
||||||
|
@IsString()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
label!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
editUrl?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
fieldRequired?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class View {
|
||||||
|
@IsString()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
label!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionsInput {
|
||||||
|
@IsString()
|
||||||
|
type!: "address" | "text" | "phone";
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class VariantField {
|
||||||
|
@IsString()
|
||||||
|
type!: BaseField;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
labelAsSafeHtml?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Variant {
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => VariantField)
|
||||||
|
fields!: VariantField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class VariantsConfig {
|
||||||
|
variants!: Record<string, Variant>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BookingField {
|
||||||
|
@IsEnum(BaseField)
|
||||||
|
type!: BaseField;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => Option)
|
||||||
|
options?: Option[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
labelAsSafeHtml?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
defaultLabel?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
getOptionsAt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => OptionsInput)
|
||||||
|
optionsInputs?: Record<string, OptionsInput>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
variant?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => VariantsConfig)
|
||||||
|
variantsConfig?: VariantsConfig;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => View)
|
||||||
|
views?: View[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
hideWhenJustOneOption?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
hidden?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(Editable)
|
||||||
|
editable?: Editable;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => Source)
|
||||||
|
sources?: Source[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecurringEvent {
|
||||||
|
@IsDate()
|
||||||
|
@IsOptional()
|
||||||
|
dtstart?: Date;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
interval!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
count!: number;
|
||||||
|
|
||||||
|
@IsEnum(Frequency)
|
||||||
|
freq!: Frequency;
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
@IsOptional()
|
||||||
|
until?: Date;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
tzid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IntervalLimits {
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
PER_DAY?: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
PER_WEEK?: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
PER_MONTH?: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
PER_YEAR?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateEventTypeInput {
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@IsOptional()
|
||||||
|
length?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
slug?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
hidden?: boolean;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EventTypeLocation)
|
||||||
|
@IsOptional()
|
||||||
|
locations?: EventTypeLocation[];
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// position?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// offsetStart?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// userId?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// profileId?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// teamId?: number;
|
||||||
|
|
||||||
|
// @IsString()
|
||||||
|
// @IsOptional()
|
||||||
|
// eventName?: string;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// parentId?: number;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsArray()
|
||||||
|
// @ValidateNested({ each: true })
|
||||||
|
// @Type(() => BookingField)
|
||||||
|
// bookingFields?: BookingField[];
|
||||||
|
|
||||||
|
// @IsString()
|
||||||
|
// @IsOptional()
|
||||||
|
// timeZone?: string;
|
||||||
|
|
||||||
|
// @IsEnum(PeriodType)
|
||||||
|
// @IsOptional()
|
||||||
|
// periodType?: PeriodType; -> import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
|
||||||
|
|
||||||
|
// @IsDate()
|
||||||
|
// @IsOptional()
|
||||||
|
// periodStartDate?: Date;
|
||||||
|
|
||||||
|
// @IsDate()
|
||||||
|
// @IsOptional()
|
||||||
|
// periodEndDate?: Date;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// periodDays?: number;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// periodCountCalendarDays?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// lockTimeZoneToggleOnBookingPage?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// requiresConfirmation?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// requiresBookerEmailVerification?: boolean;
|
||||||
|
|
||||||
|
// @ValidateNested()
|
||||||
|
// @Type(() => RecurringEvent)
|
||||||
|
// @IsOptional()
|
||||||
|
// recurringEvent?: RecurringEvent;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// disableGuests?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// hideCalendarNotes?: boolean;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @Min(0)
|
||||||
|
// @IsOptional()
|
||||||
|
// minimumBookingNotice?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// beforeEventBuffer?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// afterEventBuffer?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// seatsPerTimeSlot?: number;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// onlyShowFirstAvailableSlot?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// seatsShowAttendees?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// seatsShowAvailabilityCount?: boolean;
|
||||||
|
|
||||||
|
// @IsEnum(SchedulingType)
|
||||||
|
// @IsOptional()
|
||||||
|
// schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// scheduleId?: number;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// price?: number;
|
||||||
|
|
||||||
|
// @IsString()
|
||||||
|
// @IsOptional()
|
||||||
|
// currency?: string;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// slotInterval?: number;
|
||||||
|
|
||||||
|
// @IsString()
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsUrl()
|
||||||
|
// successRedirectUrl?: string;
|
||||||
|
|
||||||
|
// @ValidateNested()
|
||||||
|
// @Type(() => IntervalLimits)
|
||||||
|
// @IsOptional()
|
||||||
|
// bookingLimits?: IntervalLimits;
|
||||||
|
|
||||||
|
// @ValidateNested()
|
||||||
|
// @Type(() => IntervalLimits)
|
||||||
|
// @IsOptional()
|
||||||
|
// durationLimits?: IntervalLimits;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// isInstantEvent?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// assignAllTeamMembers?: boolean;
|
||||||
|
|
||||||
|
// @IsBoolean()
|
||||||
|
// @IsOptional()
|
||||||
|
// useEventTypeDestinationCalendarEmail?: boolean;
|
||||||
|
|
||||||
|
// @IsInt()
|
||||||
|
// @IsOptional()
|
||||||
|
// secondaryEmailId?: number;
|
||||||
|
}
|
||||||
@@ -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 CreateEventTypeOutput {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import {
|
||||||
|
CREATE_EVENT_DESCRIPTION_EXAMPLE,
|
||||||
|
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||||
|
CREATE_EVENT_SLUG_EXAMPLE,
|
||||||
|
CREATE_EVENT_TITLE_EXAMPLE,
|
||||||
|
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
|
import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
|
||||||
|
import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||||
|
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||||
|
import {
|
||||||
|
BookingField,
|
||||||
|
IntervalLimits,
|
||||||
|
RecurringEvent,
|
||||||
|
} from "@/ee/event-types/inputs/update-event-type.input";
|
||||||
|
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsDate,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsJSON,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
export class EventTypeOutput {
|
||||||
|
@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()
|
||||||
|
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||||
|
description!: string | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
hidden!: boolean;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EventTypeLocation)
|
||||||
|
@IsArray()
|
||||||
|
locations!: EventTypeLocation | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
position!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
offsetStart!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
userId!: number | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
profileId!: number | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
teamId!: number | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiHideProperty()
|
||||||
|
eventName!: string | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
parentId!: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => BookingField)
|
||||||
|
@ApiHideProperty()
|
||||||
|
bookingFields!: BookingField[] | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiHideProperty()
|
||||||
|
timeZone!: string | null;
|
||||||
|
|
||||||
|
@IsEnum(PeriodType)
|
||||||
|
@ApiHideProperty()
|
||||||
|
periodType!: PeriodType | null;
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
@ApiHideProperty()
|
||||||
|
periodStartDate!: Date | null;
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
@ApiHideProperty()
|
||||||
|
periodEndDate!: Date | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
periodDays!: number | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
periodCountCalendarDays!: boolean | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
requiresConfirmation!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
requiresBookerEmailVerification!: boolean;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => RecurringEvent)
|
||||||
|
@IsOptional()
|
||||||
|
@ApiHideProperty()
|
||||||
|
recurringEvent!: RecurringEvent | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
disableGuests!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
hideCalendarNotes!: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
minimumBookingNotice!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
beforeEventBuffer!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
afterEventBuffer!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
seatsPerTimeSlot!: number | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
onlyShowFirstAvailableSlot!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
seatsShowAttendees!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
seatsShowAvailabilityCount!: boolean;
|
||||||
|
|
||||||
|
@IsEnum(SchedulingType)
|
||||||
|
@ApiHideProperty()
|
||||||
|
schedulingType!: SchedulingType | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
scheduleId!: number | null;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@ApiHideProperty()
|
||||||
|
price!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiHideProperty()
|
||||||
|
currency!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
slotInterval!: number | null;
|
||||||
|
|
||||||
|
@IsJSON()
|
||||||
|
@ApiHideProperty()
|
||||||
|
metadata!: Record<string, any> | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiHideProperty()
|
||||||
|
successRedirectUrl!: string | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => IntervalLimits)
|
||||||
|
@IsOptional()
|
||||||
|
@ApiHideProperty()
|
||||||
|
bookingLimits!: IntervalLimits;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => IntervalLimits)
|
||||||
|
@ApiHideProperty()
|
||||||
|
durationLimits!: IntervalLimits;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
isInstantEvent!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
assignAllTeamMembers!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiHideProperty()
|
||||||
|
useEventTypeDestinationCalendarEmail!: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@ApiHideProperty()
|
||||||
|
secondaryEmailId!: number | 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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,26 +1,73 @@
|
|||||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
|
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
|
||||||
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
|
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||||
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesService {
|
export class EventTypesService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesRepository: EventTypesRepository,
|
private readonly eventTypesRepository: EventTypesRepository,
|
||||||
private readonly membershipsRepository: MembershipsRepository,
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
private readonly usersRepository: UsersRepository
|
private readonly usersRepository: UsersRepository,
|
||||||
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
|
private readonly dbWrite: PrismaWriteService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUserEventType(userId: number, body: CreateEventTypeInput) {
|
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput) {
|
||||||
return this.eventTypesRepository.createUserEventType(userId, body);
|
await this.checkCanCreateEventType(user.id, body);
|
||||||
|
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||||
|
const { eventType } = await createEventType({
|
||||||
|
input: body,
|
||||||
|
ctx: {
|
||||||
|
user: eventTypeUser,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
prisma: this.dbWrite.prisma,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) {
|
||||||
|
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
|
||||||
|
if (existsWithSlug) {
|
||||||
|
throw new BadRequestException("User already has an event type with this slug.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserToCreateEvent(user: UserWithProfile) {
|
||||||
|
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
||||||
|
const isOrgAdmin = organizationId
|
||||||
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
|
: false;
|
||||||
|
const profileId = user.movedToProfile?.id || null;
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
organizationId: user.organizationId,
|
||||||
|
organization: { isOrgAdmin },
|
||||||
|
profile: { id: profileId },
|
||||||
|
metadata: user.metadata,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserEventType(userId: number, eventTypeId: number) {
|
async getUserEventType(userId: number, eventTypeId: number) {
|
||||||
return this.eventTypesRepository.getUserEventType(userId, eventTypeId);
|
const eventType = await this.eventTypesRepository.getUserEventType(userId, eventTypeId);
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsEventType(userId, eventType);
|
||||||
|
return eventType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||||
@@ -30,7 +77,18 @@ export class EventTypesService {
|
|||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
return this.eventTypesRepository.getUserEventTypeForAtom(user, isUserOrganizationAdmin, eventTypeId);
|
const eventType = await this.eventTypesRepository.getUserEventTypeForAtom(
|
||||||
|
user,
|
||||||
|
isUserOrganizationAdmin,
|
||||||
|
eventTypeId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||||
|
return eventType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
||||||
@@ -43,11 +101,7 @@ export class EventTypesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createUserDefaultEventTypes(userId: number) {
|
async createUserDefaultEventTypes(userId: number) {
|
||||||
const thirtyMinutes = DEFAULT_EVENT_TYPES.thirtyMinutes;
|
const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES;
|
||||||
const thirtyMinutesVideo = DEFAULT_EVENT_TYPES.thirtyMinutesVideo;
|
|
||||||
|
|
||||||
const sixtyMinutes = DEFAULT_EVENT_TYPES.sixtyMinutes;
|
|
||||||
const sixtyMinutesVideo = DEFAULT_EVENT_TYPES.sixtyMinutesVideo;
|
|
||||||
|
|
||||||
const defaultEventTypes = await Promise.all([
|
const defaultEventTypes = await Promise.all([
|
||||||
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
|
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
|
||||||
@@ -58,4 +112,53 @@ export class EventTypesService {
|
|||||||
|
|
||||||
return defaultEventTypes;
|
return defaultEventTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
|
||||||
|
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||||
|
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||||
|
await updateEventType({
|
||||||
|
input: { id: eventTypeId, ...body },
|
||||||
|
ctx: {
|
||||||
|
user: eventTypeUser,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
prisma: this.dbWrite.prisma,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { eventType } = await this.getUserEventTypeForAtom(user, eventTypeId);
|
||||||
|
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkCanUpdateEventType(userId: number, eventTypeId: number) {
|
||||||
|
const existingEventType = await this.getUserEventType(userId, eventTypeId);
|
||||||
|
if (!existingEventType) {
|
||||||
|
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||||
|
}
|
||||||
|
this.checkUserOwnsEventType(userId, existingEventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||||
|
const profileId = user.movedToProfile?.id || null;
|
||||||
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
|
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEventType(eventTypeId: number, userId: number) {
|
||||||
|
const existingEventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||||
|
if (!existingEventType) {
|
||||||
|
throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkUserOwnsEventType(userId, existingEventType);
|
||||||
|
|
||||||
|
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserOwnsEventType(userId: number, eventType: Pick<EventType, "id" | "userId">) {
|
||||||
|
if (userId !== eventType.userId) {
|
||||||
|
throw new ForbiddenException(`User with ID=${userId} does not own event type with ID=${eventType.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
let gcalCredentials: Credential;
|
let gcalCredentials: Credential;
|
||||||
let accessTokenSecret: string;
|
let accessTokenSecret: string;
|
||||||
let refreshTokenSecret: string;
|
let refreshTokenSecret: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
@@ -82,16 +83,16 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
expect(user).toBeDefined();
|
expect(user).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/platform/gcal/oauth/auth-url`)
|
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||||
.set("Authorization", `Bearer invalid_access_token`)
|
.set("Authorization", `Bearer invalid_access_token`)
|
||||||
.expect(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
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())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/platform/gcal/oauth/auth-url`)
|
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.set("Origin", CLIENT_REDIRECT_URI)
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
@@ -99,50 +100,50 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
expect(data.authUrl).toBeDefined();
|
expect(data.authUrl).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/oauth/save: without oauth code`, async () => {
|
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(
|
.get(
|
||||||
`/api/v2/platform/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`
|
`/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`
|
||||||
)
|
)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/oauth/save: without access token`, async () => {
|
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(
|
.get(
|
||||||
`/api/v2/platform/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`
|
`/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`
|
||||||
)
|
)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/oauth/save: without origin`, async () => {
|
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(
|
.get(
|
||||||
`/api/v2/platform/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`
|
`/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`
|
||||||
)
|
)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/check with access token but without origin`, async () => {
|
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/platform/gcal/check`)
|
.get(`/api/v2/ee/gcal/check`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/check without access token`, async () => {
|
it(`/GET/ee/gcal/check without access token`, async () => {
|
||||||
await request(app.getHttpServer()).get(`/api/v2/platform/gcal/check`).expect(401);
|
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/check with access token and origin but no credentials`, async () => {
|
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/platform/gcal/check`)
|
.get(`/api/v2/ee/gcal/check`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.set("Origin", CLIENT_REDIRECT_URI)
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/platform/gcal/check with access token and origin and gcal credentials`, async () => {
|
it(`/GET/ee/gcal/check with access token and origin and gcal credentials`, async () => {
|
||||||
gcalCredentials = await credentialsRepositoryFixture.create(
|
gcalCredentials = await credentialsRepositoryFixture.create(
|
||||||
"google_calendar",
|
"google_calendar",
|
||||||
{},
|
{},
|
||||||
@@ -150,7 +151,7 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
"google-calendar"
|
"google-calendar"
|
||||||
);
|
);
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/platform/gcal/check`)
|
.get(`/api/v2/ee/gcal/check`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.set("Origin", CLIENT_REDIRECT_URI)
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||||
import { GcalService } from "@/modules/apps/services/gcal.service";
|
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
@@ -40,22 +39,21 @@ const CALENDAR_SCOPES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "platform/gcal",
|
path: "ee/gcal",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
export class GcalController {
|
export class GcalController {
|
||||||
private readonly logger = new Logger("Platform Gcal Provider");
|
private readonly logger = new Logger("Platform Gcal Provider");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly appRepository: AppsRepository,
|
|
||||||
private readonly credentialRepository: CredentialsRepository,
|
private readonly credentialRepository: CredentialsRepository,
|
||||||
private readonly tokensRepository: TokensRepository,
|
private readonly tokensRepository: TokensRepository,
|
||||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly gcalService: GcalService
|
private readonly gcalService: GCalService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private redirectUri = `${this.config.get("api.url")}/platform/gcal/oauth/save`;
|
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
||||||
|
|
||||||
@Get("/oauth/auth-url")
|
@Get("/oauth/auth-url")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { GcalController } from "@/ee/gcal/gcal.controller";
|
import { GcalController } from "@/ee/gcal/gcal.controller";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
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 { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -11,7 +11,7 @@ import { ConfigService } from "@nestjs/config";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
||||||
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GcalService],
|
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService],
|
||||||
controllers: [GcalController],
|
controllers: [GcalController],
|
||||||
})
|
})
|
||||||
export class GcalModule {}
|
export class GcalModule {}
|
||||||
@@ -65,7 +65,7 @@ describe("Me Endpoints", () => {
|
|||||||
|
|
||||||
it("should get user associated with access token", async () => {
|
it("should get user associated with access token", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get("/api/v2/me")
|
.get("/api/v2/ee/me")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
||||||
@@ -84,7 +84,7 @@ describe("Me Endpoints", () => {
|
|||||||
const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" };
|
const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" };
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch("/api/v2/me")
|
.patch("/api/v2/ee/me")
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
@@ -108,19 +108,22 @@ describe("Me Endpoints", () => {
|
|||||||
it("should not update user associated with access token given invalid timezone", async () => {
|
it("should not update user associated with access token given invalid timezone", async () => {
|
||||||
const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" };
|
const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
|
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid time format", async () => {
|
it("should not update user associated with access token given invalid time format", async () => {
|
||||||
const bodyWithIncorrectTimeFormat: UpdateManagedPlatformUserInput = { timeFormat: 100 };
|
const bodyWithIncorrectTimeFormat: UpdateManagedPlatformUserInput = { timeFormat: 100 };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
return request(app.getHttpServer())
|
||||||
|
.patch("/api/v2/ee/me")
|
||||||
|
.send(bodyWithIncorrectTimeFormat)
|
||||||
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid week start", async () => {
|
it("should not update user associated with access token given invalid week start", async () => {
|
||||||
const bodyWithIncorrectWeekStart: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" };
|
const bodyWithIncorrectWeekStart: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { UserResponse, userSchemaResponse } from "@calcom/platform-types";
|
|||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "me",
|
path: "ee/me",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -9,7 +8,6 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Param,
|
Param,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
@@ -20,16 +18,11 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
|||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "platform/provider",
|
path: "ee/provider",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
export class CalProviderController {
|
export class CalProviderController {
|
||||||
private readonly logger = new Logger("Platform Provider Controller");
|
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly tokensRepository: TokensRepository,
|
|
||||||
private readonly oauthClientRepository: OAuthClientRepository
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get("/:clientId")
|
@Get("/:clientId")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
|
|
||||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries";
|
import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries";
|
||||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
|
||||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
||||||
import type { UpdateScheduleOutputType } from "@calcom/platform-libraries";
|
import type { UpdateScheduleOutputType } from "@calcom/platform-libraries";
|
||||||
import { ApiSuccessResponse, ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
import { ApiSuccessResponse, ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
||||||
@@ -73,16 +72,6 @@ export class SchedulesController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("/time-zones")
|
|
||||||
async getTimeZones(): Promise<ApiResponse<CityTimezones>> {
|
|
||||||
const timeZones = await this.schedulesService.getSchedulePossibleTimeZones();
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: timeZones,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("/:scheduleId")
|
@Get("/:scheduleId")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getSchedule(
|
async getSchedule(
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
|||||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Schedule } from "@prisma/client";
|
import { Schedule } from "@prisma/client";
|
||||||
|
|
||||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesService {
|
export class SchedulesService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -96,8 +94,4 @@ export class SchedulesService {
|
|||||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSchedulePossibleTimeZones() {
|
|
||||||
return cityTimezonesHandler();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ export type Environment = {
|
|||||||
NEXTAUTH_SECRET: string;
|
NEXTAUTH_SECRET: string;
|
||||||
DATABASE_URL: string;
|
DATABASE_URL: string;
|
||||||
JWT_SECRET: string;
|
JWT_SECRET: string;
|
||||||
SENTRY_DNS: string;
|
SENTRY_DSN: string;
|
||||||
LOG_LEVEL: keyof typeof logLevels;
|
LOG_LEVEL: keyof typeof logLevels;
|
||||||
REDIS_URL: string;
|
REDIS_URL: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
@@ -10,10 +11,14 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
|||||||
catch(exception: HttpException, host: ArgumentsHost) {
|
catch(exception: HttpException, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
const request = ctx.getRequest();
|
const request = ctx.getRequest<Request>();
|
||||||
const statusCode = exception.getStatus();
|
const statusCode = exception.getStatus();
|
||||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
|
body: request.body,
|
||||||
|
headers: request.headers,
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
});
|
});
|
||||||
response.status(statusCode).json({
|
response.status(statusCode).json({
|
||||||
status: ERROR_STATUS,
|
status: ERROR_STATUS,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
PrismaClientUnknownRequestError,
|
PrismaClientUnknownRequestError,
|
||||||
PrismaClientValidationError,
|
PrismaClientValidationError,
|
||||||
} from "@prisma/client/runtime/library";
|
} from "@prisma/client/runtime/library";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
@@ -31,9 +32,13 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
|||||||
catch(error: PrismaError, host: ArgumentsHost) {
|
catch(error: PrismaError, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
const request = ctx.getRequest();
|
const request = ctx.getRequest<Request>();
|
||||||
this.logger.error(`PrismaError: ${error.message}`, {
|
this.logger.error(`PrismaError: ${error.message}`, {
|
||||||
error,
|
error,
|
||||||
|
body: request.body,
|
||||||
|
headers: request.headers,
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
});
|
});
|
||||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||||
status: ERROR_STATUS,
|
status: ERROR_STATUS,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
|
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
|
||||||
import { BaseExceptionFilter } from "@nestjs/core";
|
import { BaseExceptionFilter } from "@nestjs/core";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||||
import { Response } from "@calcom/platform-types";
|
import { Response } from "@calcom/platform-types";
|
||||||
@@ -12,10 +13,14 @@ export class SentryFilter extends BaseExceptionFilter {
|
|||||||
handleUnknownError(exception: any, host: ArgumentsHost): void {
|
handleUnknownError(exception: any, host: ArgumentsHost): void {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
const request = ctx.getRequest();
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
|
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
|
body: request.body,
|
||||||
|
headers: request.headers,
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
});
|
});
|
||||||
|
|
||||||
// capture if client has been init
|
// capture if client has been init
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { TRPCError } from "@calcom/platform-libraries";
|
import { TRPCError } from "@calcom/platform-libraries";
|
||||||
@@ -11,7 +12,8 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
|||||||
catch(exception: TRPCError, host: ArgumentsHost) {
|
catch(exception: TRPCError, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
const request = ctx.getRequest();
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
let statusCode = 500;
|
let statusCode = 500;
|
||||||
switch (exception.code) {
|
switch (exception.code) {
|
||||||
case "UNAUTHORIZED":
|
case "UNAUTHORIZED":
|
||||||
@@ -38,9 +40,15 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
|||||||
statusCode = 500;
|
statusCode = 500;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||||
exception,
|
exception,
|
||||||
|
body: request.body,
|
||||||
|
headers: request.headers,
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(statusCode).json({
|
response.status(statusCode).json({
|
||||||
status: ERROR_STATUS,
|
status: ERROR_STATUS,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||||
|
import { Request } from "express";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { BAD_REQUEST, ERROR_STATUS } from "@calcom/platform-constants";
|
import { BAD_REQUEST, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
@@ -12,10 +13,16 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
|||||||
catch(error: ZodError, host: ArgumentsHost) {
|
catch(error: ZodError, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
const request = ctx.getRequest();
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
this.logger.error(`ZodError: ${error.message}`, {
|
this.logger.error(`ZodError: ${error.message}`, {
|
||||||
error,
|
error,
|
||||||
|
body: request.body,
|
||||||
|
headers: request.headers,
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.status(HttpStatus.BAD_REQUEST).json({
|
response.status(HttpStatus.BAD_REQUEST).json({
|
||||||
status: ERROR_STATUS,
|
status: ERROR_STATUS,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
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();
|
const request = ctx.switchToHttp().getRequest();
|
||||||
return request.query.for === "atom";
|
return request.query.for === "atom";
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
|
||||||
export class BaseStrategy {
|
export class BaseStrategy {
|
||||||
success!: (user: unknown) => void;
|
success!: (user: unknown) => void;
|
||||||
error!: (error: Error) => void;
|
error!: (error: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NextAuthPassportStrategy {
|
||||||
|
success!: (user: UserWithProfile) => void;
|
||||||
|
error!: (error: Error) => void;
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import { google } from "googleapis";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GcalService {
|
export class GCalService {
|
||||||
private logger = new Logger("GcalService");
|
private logger = new Logger("GcalService");
|
||||||
|
|
||||||
|
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||||
|
|
||||||
constructor(private readonly appsRepository: AppsRepository) {}
|
constructor(private readonly appsRepository: AppsRepository) {}
|
||||||
|
|
||||||
async getOAuthClient(redirectUri: string) {
|
async getOAuthClient(redirectUri: string) {
|
||||||
@@ -17,9 +19,7 @@ export class GcalService {
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { client_id, client_secret } = z
|
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||||
.object({ client_id: z.string(), client_secret: z.string() })
|
|
||||||
.parse(app.keys);
|
|
||||||
|
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||||
return oAuth2Client;
|
return oAuth2Client;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class OrganizationRolesGuard implements CanActivate {
|
|||||||
async isPlatform(organizationId: number) {
|
async isPlatform(organizationId: number) {
|
||||||
const isPlatform = await this.organizationsService.isPlatform(organizationId);
|
const isPlatform = await this.organizationsService.isPlatform(organizationId);
|
||||||
if (!isPlatform) {
|
if (!isPlatform) {
|
||||||
throw new ForbiddenException("Organization is not a platform.");
|
throw new ForbiddenException("Organization is not a platform (SHP).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
@@ -7,7 +7,7 @@ import type { Request } from "express";
|
|||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
|
export class NextAuthStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
|
||||||
constructor(private readonly userRepository: UsersRepository, private readonly config: ConfigService) {
|
constructor(private readonly userRepository: UsersRepository, private readonly config: ConfigService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth"
|
|||||||
throw new UnauthorizedException("Email not found in the authentication token.");
|
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.userRepository.findByEmail(payload.email);
|
const user = await this.userRepository.findByEmailWithProfile(payload.email);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||||
import { EventsModule } from "@/modules/events/events.module";
|
import { EventsModule } from "@/modules/events/events.module";
|
||||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||||
|
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule],
|
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
||||||
})
|
})
|
||||||
export class EndpointsModule implements NestModule {
|
export class EndpointsModule implements NestModule {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ export class EventsController {
|
|||||||
queryParams.eventSlug,
|
queryParams.eventSlug,
|
||||||
queryParams.isTeamEvent,
|
queryParams.isTeamEvent,
|
||||||
queryParams.org || null,
|
queryParams.org || null,
|
||||||
this.prismaReadService.prisma as unknown as PrismaClient
|
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 {
|
return {
|
||||||
data: event,
|
data: event,
|
||||||
|
|||||||
+15
@@ -179,6 +179,21 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it(`/GET: return list of managed users`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<UserReturned[]> = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toBeGreaterThan(0);
|
||||||
|
expect(responseBody.data[0].email).toEqual(postResponseData.user.email);
|
||||||
|
});
|
||||||
|
|
||||||
it(`/GET/:id`, async () => {
|
it(`/GET/:id`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||||
|
|||||||
+23
-1
@@ -20,11 +20,12 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
Delete,
|
Delete,
|
||||||
|
Query,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse, Pagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "oauth-clients/:clientId/users",
|
path: "oauth-clients/:clientId/users",
|
||||||
@@ -39,6 +40,27 @@ export class OAuthClientUsersController {
|
|||||||
private readonly oauthRepository: OAuthClientRepository
|
private readonly oauthRepository: OAuthClientRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@UseGuards(OAuthClientCredentialsGuard)
|
||||||
|
async getManagedUsers(
|
||||||
|
@Param("clientId") oAuthClientId: string,
|
||||||
|
@Query() queryParams: Pagination
|
||||||
|
): Promise<ApiResponse<User[]>> {
|
||||||
|
this.logger.log(`getting managed users with data for OAuth Client with ID ${oAuthClientId}`);
|
||||||
|
const { offset, limit } = queryParams;
|
||||||
|
|
||||||
|
const existingUsers = await this.userRepository.findManagedUsersByOAuthClientId(
|
||||||
|
oAuthClientId,
|
||||||
|
offset ?? 0,
|
||||||
|
limit ?? 50
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: existingUsers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@UseGuards(OAuthClientCredentialsGuard)
|
@UseGuards(OAuthClientCredentialsGuard)
|
||||||
async createUser(
|
async createUser(
|
||||||
|
|||||||
+6
-4
@@ -8,6 +8,7 @@ import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/o
|
|||||||
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
||||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@@ -57,9 +58,10 @@ export class OAuthClientsController {
|
|||||||
type: CreateOAuthClientResponseDto,
|
type: CreateOAuthClientResponseDto,
|
||||||
})
|
})
|
||||||
async createOAuthClient(
|
async createOAuthClient(
|
||||||
@GetUser("organizationId") organizationId: number,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() body: CreateOAuthClientInput
|
@Body() body: CreateOAuthClientInput
|
||||||
): Promise<CreateOAuthClientResponseDto> {
|
): Promise<CreateOAuthClientResponseDto> {
|
||||||
|
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||||
);
|
);
|
||||||
@@ -77,9 +79,9 @@ export class OAuthClientsController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
@Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||||
async getOAuthClients(
|
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
||||||
@GetUser("organizationId") organizationId: number
|
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
||||||
): Promise<GetOAuthClientsResponseDto> {
|
|
||||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||||
return { status: SUCCESS_STATUS, data: clients };
|
return { status: SUCCESS_STATUS, data: clients };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EventTypesService } from "@/ee/event-types/services/event-types.service
|
|||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
@@ -24,24 +24,24 @@ export class OAuthClientUsersService {
|
|||||||
) {
|
) {
|
||||||
let user: User;
|
let user: User;
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
const username = generateShortHash(body.email, oAuthClientId);
|
throw new BadRequestException("You cannot create a managed user outside of an organization");
|
||||||
user = await this.userRepository.create(body, username, oAuthClientId, isPlatformManaged);
|
|
||||||
} else {
|
} else {
|
||||||
const [_, emailDomain] = body.email.split("@");
|
const [username, emailDomain] = body.email.split("@");
|
||||||
|
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
||||||
user = (
|
user = (
|
||||||
await createNewUsersConnectToOrgIfExists({
|
await createNewUsersConnectToOrgIfExists({
|
||||||
usernamesOrEmails: [body.email],
|
usernamesOrEmails: [email],
|
||||||
input: {
|
input: {
|
||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
role: "MEMBER",
|
role: "MEMBER",
|
||||||
usernameOrEmail: [body.email],
|
usernameOrEmail: [email],
|
||||||
isOrg: true,
|
isOrg: true,
|
||||||
language: "en",
|
language: "en",
|
||||||
},
|
},
|
||||||
parentId: null,
|
parentId: null,
|
||||||
autoAcceptEmailDomain: emailDomain,
|
autoAcceptEmailDomain: "never-auto-accept-email-domain-for-managed-users",
|
||||||
connectionInfoMap: {
|
connectionInfoMap: {
|
||||||
[body.email]: {
|
[email]: {
|
||||||
orgId: organizationId,
|
orgId: organizationId,
|
||||||
autoAccept: true,
|
autoAccept: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { TokenExpiredException } from "@/modules/auth/guards/access-token/token-expired.exception";
|
import { TokenExpiredException } from "@/modules/auth/guards/access-token/token-expired.exception";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
|
|
||||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuthFlowService {
|
export class OAuthFlowService {
|
||||||
private logger = new Logger("OAuthFlowService");
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly tokensRepository: TokensRepository,
|
private readonly tokensRepository: TokensRepository,
|
||||||
private readonly oAuthClientRepository: OAuthClientRepository
|
private readonly oAuthClientRepository: OAuthClientRepository //private readonly redisService: RedisIOService
|
||||||
) //private readonly redisService: RedisIOService
|
) {}
|
||||||
{}
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async propagateAccessToken(accessToken: string) {
|
async propagateAccessToken(accessToken: string) {
|
||||||
// this.logger.log("Propagating access token to redis", accessToken);
|
// this.logger.log("Propagating access token to redis", accessToken);
|
||||||
// TODO propagate
|
// TODO propagate
|
||||||
@@ -48,6 +46,7 @@ export class OAuthFlowService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
private async readFromCache(secret: string) {
|
private async readFromCache(secret: string) {
|
||||||
return { status: "CACHE_MISS" };
|
return { status: "CACHE_MISS" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
|
|||||||
export class PrismaReadService implements OnModuleInit {
|
export class PrismaReadService implements OnModuleInit {
|
||||||
public prisma: PrismaClient;
|
public prisma: PrismaClient;
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {
|
constructor(readonly configService: ConfigService) {
|
||||||
const dbUrl = configService.get("db.readUrl", { infer: true });
|
const dbUrl = configService.get("db.readUrl", { infer: true });
|
||||||
|
|
||||||
this.prisma = new PrismaClient({
|
this.prisma = new PrismaClient({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
|
|||||||
export class PrismaWriteService implements OnModuleInit {
|
export class PrismaWriteService implements OnModuleInit {
|
||||||
public prisma: PrismaClient;
|
public prisma: PrismaClient;
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {
|
constructor(readonly configService: ConfigService) {
|
||||||
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
||||||
|
|
||||||
this.prisma = new PrismaClient({
|
this.prisma = new PrismaClient({
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [PrismaModule],
|
||||||
providers: [SelectedCalendarsRepository],
|
providers: [SelectedCalendarsRepository],
|
||||||
exports: [SelectedCalendarsRepository],
|
exports: [SelectedCalendarsRepository],
|
||||||
})
|
})
|
||||||
export class CredentialsModule {}
|
export class SelectedCalendarsModule {}
|
||||||
@@ -16,4 +16,12 @@ export class SelectedCalendarsRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserSelectedCalendars(userId: number) {
|
||||||
|
return this.dbRead.prisma.selectedCalendar.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||||
|
import { Controller, Get } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "timezones",
|
||||||
|
version: "2",
|
||||||
|
})
|
||||||
|
export class TimezonesController {
|
||||||
|
constructor(private readonly timezonesService: TimezonesService) {}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
async getTimeZones(): Promise<ApiResponse<CityTimezones>> {
|
||||||
|
const timeZones = await this.timezonesService.getCityTimeZones();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: timeZones,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TimezonesService {
|
||||||
|
async getCityTimeZones() {
|
||||||
|
return cityTimezonesHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TimezonesController } from "@/modules/timezones/controllers/timezones.controller";
|
||||||
|
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
providers: [TimezonesService],
|
||||||
|
controllers: [TimezonesController],
|
||||||
|
exports: [TimezonesService],
|
||||||
|
})
|
||||||
|
export class TimezoneModule {}
|
||||||
@@ -48,7 +48,7 @@ export class TokensRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createOAuthTokens(clientId: string, ownerId: number) {
|
async createOAuthTokens(clientId: string, ownerId: number) {
|
||||||
const accessExpiry = DateTime.now().plus({ minute: 1 }).startOf("minute").toJSDate();
|
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||||
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
|
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
|
||||||
this.dbWrite.prisma.accessToken.create({
|
this.dbWrite.prisma.accessToken.create({
|
||||||
@@ -101,7 +101,7 @@ export class TokensRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) {
|
async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) {
|
||||||
const accessExpiry = DateTime.now().plus({ minute: 1 }).startOf("minute").toJSDate();
|
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class UsersRepository {
|
|||||||
) {
|
) {
|
||||||
this.formatInput(user);
|
this.formatInput(user);
|
||||||
|
|
||||||
return this.dbRead.prisma.user.create({
|
return this.dbWrite.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
...user,
|
...user,
|
||||||
username,
|
username,
|
||||||
@@ -34,7 +34,7 @@ export class UsersRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addToOAuthClient(userId: number, oAuthClientId: string) {
|
async addToOAuthClient(userId: number, oAuthClientId: string) {
|
||||||
return this.dbRead.prisma.user.update({
|
return this.dbWrite.prisma.user.update({
|
||||||
data: {
|
data: {
|
||||||
platformOAuthClients: {
|
platformOAuthClients: {
|
||||||
connect: { id: oAuthClientId },
|
connect: { id: oAuthClientId },
|
||||||
@@ -83,6 +83,17 @@ export class UsersRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findByEmailWithProfile(email: string) {
|
||||||
|
return this.dbRead.prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
movedToProfile: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findByUsername(username: string) {
|
async findByUsername(username: string) {
|
||||||
return this.dbRead.prisma.user.findFirst({
|
return this.dbRead.prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -91,6 +102,21 @@ export class UsersRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findManagedUsersByOAuthClientId(oauthClientId: string, cursor: number, limit: number) {
|
||||||
|
return this.dbRead.prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
platformOAuthClients: {
|
||||||
|
some: {
|
||||||
|
id: oauthClientId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isPlatformManaged: true,
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
skip: cursor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async update(userId: number, updateData: UpdateManagedPlatformUserInput) {
|
async update(userId: number, updateData: UpdateManagedPlatformUserInput) {
|
||||||
this.formatInput(updateData);
|
this.formatInput(updateData);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,40 @@
|
|||||||
"/api/v2/events/public": {
|
"/api/v2/events/public": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "EventsController_getPublicEvent",
|
"operationId": "EventsController_getPublicEvent",
|
||||||
"parameters": [],
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventSlug",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isTeamEvent",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "org",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "",
|
"description": "",
|
||||||
@@ -38,6 +71,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/oauth-clients/{clientId}/users": {
|
"/api/v2/oauth-clients/{clientId}/users": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "OAuthClientUsersController_getManagedUsers",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "clientId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "OAuthClientUsersController_createUser",
|
"operationId": "OAuthClientUsersController_createUser",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@@ -55,7 +113,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/CreateUserInput"
|
"$ref": "#/components/schemas/CreateManagedPlatformUserInput"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +191,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UpdateUserInput"
|
"$ref": "#/components/schemas/UpdateManagedPlatformUserInput"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,12 +558,15 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object"
|
"$ref": "#/components/schemas/CreateEventTypeOutput"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tags": [
|
||||||
|
"Event types"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "EventTypesController_getEventTypes",
|
"operationId": "EventTypesController_getEventTypes",
|
||||||
@@ -516,12 +577,15 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object"
|
"$ref": "#/components/schemas/GetEventTypesOutput"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tags": [
|
||||||
|
"Event types"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/event-types/{eventTypeId}": {
|
"/api/v2/event-types/{eventTypeId}": {
|
||||||
@@ -543,12 +607,81 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object"
|
"$ref": "#/components/schemas/GetEventTypeOutput"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tags": [
|
||||||
|
"Event types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"operationId": "EventTypesController_updateEventType",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateEventTypeInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateEventTypeOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Event types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "EventTypesController_deleteEventType",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeleteEventTypeOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Event types"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/event-types/{username}/public": {
|
"/api/v2/event-types/{username}/public": {
|
||||||
@@ -570,15 +703,18 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object"
|
"$ref": "#/components/schemas/GetEventTypesPublicOutput"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tags": [
|
||||||
|
"Event types"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/platform/gcal/oauth/auth-url": {
|
"/api/v2/ee/gcal/oauth/auth-url": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "GcalController_redirect",
|
"operationId": "GcalController_redirect",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@@ -605,7 +741,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/platform/gcal/oauth/save": {
|
"/api/v2/ee/gcal/oauth/save": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "GcalController_save",
|
"operationId": "GcalController_save",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@@ -640,7 +776,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/platform/gcal/check": {
|
"/api/v2/ee/gcal/check": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "GcalController_check",
|
"operationId": "GcalController_check",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
@@ -658,7 +794,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/platform/provider/{clientId}": {
|
"/api/v2/ee/provider/{clientId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "CalProviderController_verifyClientId",
|
"operationId": "CalProviderController_verifyClientId",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@@ -685,7 +821,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/platform/provider/{clientId}/access-token": {
|
"/api/v2/ee/provider/{clientId}/access-token": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "CalProviderController_verifyAccessToken",
|
"operationId": "CalProviderController_verifyAccessToken",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@@ -774,24 +910,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/schedules/time-zones": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "SchedulesController_getTimeZones",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v2/schedules/{scheduleId}": {
|
"/api/v2/schedules/{scheduleId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "SchedulesController_getSchedule",
|
"operationId": "SchedulesController_getSchedule",
|
||||||
@@ -870,7 +988,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/me": {
|
"/api/v2/ee/me": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "MeController_getMe",
|
"operationId": "MeController_getMe",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
@@ -895,7 +1013,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UpdateUserInput"
|
"$ref": "#/components/schemas/UpdateManagedPlatformUserInput"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -951,6 +1069,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v2/ee/bookings": {
|
"/api/v2/ee/bookings": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "BookingsController_getBookings",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "BookingsController_createBooking",
|
"operationId": "BookingsController_createBooking",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
@@ -978,6 +1112,97 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v2/ee/bookings/{bookingUid}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "BookingsController_getBooking",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bookingUid",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/ee/bookings/{bookingUid}/reschedule": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "BookingsController_getBookingForReschedule",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bookingUid",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/ee/bookings/{bookingId}/cancel": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "BookingsController_cancelBooking",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bookingId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CancelBookingInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v2/ee/bookings/reccuring": {
|
"/api/v2/ee/bookings/reccuring": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "BookingsController_createReccuringBooking",
|
"operationId": "BookingsController_createReccuringBooking",
|
||||||
@@ -1100,6 +1325,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/timezones": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "TimezonesController_getTimeZones",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
@@ -1112,7 +1355,7 @@
|
|||||||
"servers": [],
|
"servers": [],
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"CreateUserInput": {
|
"CreateManagedPlatformUserInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"email": {
|
"email": {
|
||||||
@@ -1124,9 +1367,6 @@
|
|||||||
"timeFormat": {
|
"timeFormat": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"defaultScheduleId": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"weekStart": {
|
"weekStart": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1138,7 +1378,7 @@
|
|||||||
"email"
|
"email"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"UpdateUserInput": {
|
"UpdateManagedPlatformUserInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"email": {
|
"email": {
|
||||||
@@ -1187,11 +1427,11 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "success",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"success",
|
"success",
|
||||||
"error"
|
"error"
|
||||||
]
|
],
|
||||||
|
"example": "success"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"example": {
|
"example": {
|
||||||
@@ -1230,7 +1470,8 @@
|
|||||||
"example": 3
|
"example": 3
|
||||||
},
|
},
|
||||||
"logo": {
|
"logo": {
|
||||||
"type": "object",
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
"example": "https://example.com/logo.png"
|
"example": "https://example.com/logo.png"
|
||||||
},
|
},
|
||||||
"redirectUris": {
|
"redirectUris": {
|
||||||
@@ -1393,7 +1634,251 @@
|
|||||||
"refreshToken"
|
"refreshToken"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"EventTypeLocation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "link"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "https://masterchief.com/argentina/flan/video/9129412"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
"CreateEventTypeInput": {
|
"CreateEventTypeInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"length": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"example": 60
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "cooking-class"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Learn the secrets of masterchief!"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/EventTypeLocation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"length",
|
||||||
|
"slug",
|
||||||
|
"title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"EventTypeOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"length": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 60
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "cooking-class"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Learn the secrets of masterchief!"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"nullable": true,
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/EventTypeLocation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"length",
|
||||||
|
"slug",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"locations"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CreateEventTypeOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/EventTypeOutput"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"eventType": {
|
||||||
|
"$ref": "#/components/schemas/EventTypeOutput"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"eventType"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetEventTypeOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/Data"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"EventTypeGroup": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"eventTypes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/EventTypeOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"eventTypes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetEventTypesData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"eventTypeGroups": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/EventTypeGroup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"eventTypeGroups"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetEventTypesOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/GetEventTypesData"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PublicEventType": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"length": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 60
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "cooking-class"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Learn the secrets of masterchief!"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"length",
|
||||||
|
"slug",
|
||||||
|
"title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetEventTypesPublicOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/PublicEventType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UpdateEventTypeInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"length": {
|
"length": {
|
||||||
@@ -1405,14 +1890,88 @@
|
|||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/EventTypeLocation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateEventTypeOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/EventTypeOutput"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DeleteData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"length": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 60
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "cooking-class"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Learn the secrets of masterchief!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
"length",
|
"length",
|
||||||
"slug",
|
"slug",
|
||||||
"title"
|
"title"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"DeleteEventTypeOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "success",
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/DeleteData"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
"CreateAvailabilityInput": {
|
"CreateAvailabilityInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1526,6 +2085,10 @@
|
|||||||
"hashedLink"
|
"hashedLink"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"CancelBookingInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
},
|
||||||
"ReserveSlotInput": {
|
"ReserveSlotInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { TestingModule } from "@nestjs/testing";
|
||||||
|
import { Booking, User } from "@prisma/client";
|
||||||
|
|
||||||
|
export class BookingsRepositoryFixture {
|
||||||
|
private primaReadClient: PrismaReadService["prisma"];
|
||||||
|
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||||
|
|
||||||
|
constructor(private readonly module: TestingModule) {
|
||||||
|
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||||
|
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(bookingId: Booking["id"]) {
|
||||||
|
return this.primaReadClient.booking.findFirst({ where: { id: bookingId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteById(bookingId: Booking["id"]) {
|
||||||
|
return this.prismaWriteClient.booking.delete({ where: { id: bookingId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllBookings(userId: User["id"], userEmail: User["email"]) {
|
||||||
|
return this.prismaWriteClient.booking.deleteMany({ where: { userId, userPrimaryEmail: userEmail } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export class EventTypesRepositoryFixture {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateEventTypeInput, userId: number) {
|
async create(data: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">, userId: number) {
|
||||||
return this.prismaWriteClient.eventType.create({
|
return this.prismaWriteClient.eventType.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NextAuthMockStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
|
export class NextAuthMockStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
|
||||||
constructor(private readonly email: string, private readonly userRepository: UsersRepository) {
|
constructor(private readonly email: string, private readonly userRepository: UsersRepository) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
async authenticate() {
|
async authenticate() {
|
||||||
try {
|
try {
|
||||||
const user = await this.userRepository.findByEmail(this.email);
|
const user = await this.userRepository.findByEmailWithProfile(this.email);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("User with the provided email not found");
|
throw new Error("User with the provided email not found");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const env: Partial<Omit<Environment, "NODE_ENV">> = {
|
|||||||
LOG_LEVEL: "trace",
|
LOG_LEVEL: "trace",
|
||||||
REDIS_URL: "redis://localhost:9199",
|
REDIS_URL: "redis://localhost:9199",
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
process.env = {
|
process.env = {
|
||||||
...env,
|
...env,
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
## Cal.com Platform Example (https://platform.cal.com)
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import LegacyPage, { WrappedSetPasswordPage } from "@pages/settings/organizations/[id]/set-password";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
|
||||||
import { WithLayout } from "app/layoutHOC";
|
|
||||||
|
|
||||||
export const generateMetadata = async () =>
|
|
||||||
await _generateMetadata(
|
|
||||||
(t) => t("set_a_password"),
|
|
||||||
(t) => t("set_a_password_description")
|
|
||||||
);
|
|
||||||
|
|
||||||
export default WithLayout({ Page: LegacyPage, getLayout: WrappedSetPasswordPage });
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { X } from "@calcom/ui/components/icon";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
export default function AddToHomescreen() {
|
export default function AddToHomescreen() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||||
<span className="sr-only">{t("dismiss")}</span>
|
<span className="sr-only">{t("dismiss")}</span>
|
||||||
<X className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
<Icon name="x" className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
|||||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { Badge, ListItemText, Avatar } from "@calcom/ui";
|
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
|
||||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
type ShouldHighlight =
|
type ShouldHighlight =
|
||||||
| {
|
| {
|
||||||
@@ -105,7 +104,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
<ListItemText component="p">{description}</ListItemText>
|
<ListItemText component="p">{description}</ListItemText>
|
||||||
{invalidCredential && (
|
{invalidCredential && (
|
||||||
<div className="flex gap-x-2 pt-2">
|
<div className="flex gap-x-2 pt-2">
|
||||||
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||||
{t("invalid_credential")}
|
{t("invalid_credential")}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
|
|||||||
@@ -3,40 +3,39 @@
|
|||||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { UpgradeTip } from "@calcom/features/tips";
|
import { UpgradeTip } from "@calcom/features/tips";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, ButtonGroup } from "@calcom/ui";
|
import { Button, ButtonGroup, Icon } from "@calcom/ui";
|
||||||
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
export default function EnterprisePage() {
|
export default function EnterprisePage() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
icon: <Globe className="h-5 w-5 text-red-500" />,
|
icon: <Icon name="globe" className="h-5 w-5 text-red-500" />,
|
||||||
title: t("branded_subdomain"),
|
title: t("branded_subdomain"),
|
||||||
description: t("branded_subdomain_description"),
|
description: t("branded_subdomain_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BarChart className="h-5 w-5 text-blue-500" />,
|
icon: <Icon name="bar-chart" className="h-5 w-5 text-blue-500" />,
|
||||||
title: t("org_insights"),
|
title: t("org_insights"),
|
||||||
description: t("org_insights_description"),
|
description: t("org_insights_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
|
icon: <Icon name="paintbrush" className="h-5 w-5 text-pink-500" />,
|
||||||
title: t("extensive_whitelabeling"),
|
title: t("extensive_whitelabeling"),
|
||||||
description: t("extensive_whitelabeling_description"),
|
description: t("extensive_whitelabeling_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Users className="h-5 w-5 text-orange-500" />,
|
icon: <Icon name="users" className="h-5 w-5 text-orange-500" />,
|
||||||
title: t("unlimited_teams"),
|
title: t("unlimited_teams"),
|
||||||
description: t("unlimited_teams_description"),
|
description: t("unlimited_teams_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CreditCard className="h-5 w-5 text-green-500" />,
|
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
|
||||||
title: t("unified_billing"),
|
title: t("unified_billing"),
|
||||||
description: t("unified_billing_description"),
|
description: t("unified_billing_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Lock className="h-5 w-5 text-purple-500" />,
|
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
|
||||||
title: t("advanced_managed_events"),
|
title: t("advanced_managed_events"),
|
||||||
description: t("advanced_managed_events_description"),
|
description: t("advanced_managed_events_description"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import { trpc } from "@calcom/trpc/react";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Plus } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
|
|||||||
return (
|
return (
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
|
<Button StartIcon="plus" color="secondary" {...(isPending && { loading: isPending })}>
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -50,7 +49,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
|
|||||||
{options.map((data) => (
|
{options.map((data) => (
|
||||||
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||||
{data.slug === "add-new" ? (
|
{data.slug === "add-new" ? (
|
||||||
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
|
<DropdownItem StartIcon="plus" color="minimal" href="/apps/categories/calendar">
|
||||||
{t("install_new_calendar_app")}
|
{t("install_new_calendar_app")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -11,17 +11,16 @@ import type { AppCategories } from "@calcom/prisma/enums";
|
|||||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||||
import type { App } from "@calcom/types/App";
|
import type { App } from "@calcom/types/App";
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
List,
|
List,
|
||||||
showToast,
|
showToast,
|
||||||
Button,
|
|
||||||
DropdownMenuItem,
|
|
||||||
Alert,
|
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import AppListCard from "@components/AppListCard";
|
import AppListCard from "@components/AppListCard";
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
<Button StartIcon="ellipsis" variant="icon" color="secondary" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
|
{!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
|
||||||
@@ -90,7 +89,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={Video}
|
StartIcon="video"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
|
const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
|
||||||
if (locationType?.linkType === "static") {
|
if (locationType?.linkType === "static") {
|
||||||
@@ -214,7 +213,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
|||||||
color="destructive"
|
color="destructive"
|
||||||
onClick={() => handleDisconnect(credentialId, teamId)}
|
onClick={() => handleDisconnect(credentialId, teamId)}
|
||||||
disabled={isGlobal}
|
disabled={isGlobal}
|
||||||
StartIcon={Trash}>
|
StartIcon="trash">
|
||||||
{t("remove_app")}
|
{t("remove_app")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { IframeHTMLAttributes } from "react";
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
|
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
|
||||||
@@ -10,8 +10,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
import { Badge, Button, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
|
import { Badge, Button, Icon, SkeletonButton, SkeletonText, showToast } from "@calcom/ui";
|
||||||
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { InstallAppButtonChild } from "./InstallAppButtonChild";
|
import { InstallAppButtonChild } from "./InstallAppButtonChild";
|
||||||
|
|
||||||
@@ -196,7 +195,7 @@ export const AppPage = ({
|
|||||||
isGlobal ||
|
isGlobal ||
|
||||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Button StartIcon={Check} color="secondary" disabled>
|
<Button StartIcon="check" color="secondary" disabled>
|
||||||
{existingCredentials.length > 0
|
{existingCredentials.length > 0
|
||||||
? t("active_install", { count: existingCredentials.length })
|
? t("active_install", { count: existingCredentials.length })
|
||||||
: t("default")}
|
: t("default")}
|
||||||
@@ -323,7 +322,7 @@ export const AppPage = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||||
href={docs}>
|
href={docs}>
|
||||||
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
<Icon name="book-open" className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||||
{t("documentation")}
|
{t("documentation")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -335,7 +334,7 @@ export const AppPage = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={website}>
|
href={website}>
|
||||||
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
<Icon name="external-link" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
{website.replace("https://", "")}
|
{website.replace("https://", "")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -347,7 +346,7 @@ export const AppPage = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={`mailto:${email}`}>
|
href={`mailto:${email}`}>
|
||||||
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
<Icon name="mail" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
@@ -360,7 +359,7 @@ export const AppPage = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={tos}>
|
href={tos}>
|
||||||
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
<Icon name="file" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
{t("terms_of_service")}
|
{t("terms_of_service")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -372,7 +371,7 @@ export const AppPage = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={privacy}>
|
href={privacy}>
|
||||||
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
<Icon name="shield" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
{t("privacy_policy")}
|
{t("privacy_policy")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -383,7 +382,7 @@ export const AppPage = ({
|
|||||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||||
</span>
|
</span>
|
||||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||||
<Flag className="inline h-3 w-3" /> {t("report_app")}
|
<Icon name="flag" className="inline h-3 w-3" /> {t("report_app")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
List,
|
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
|
||||||
ShellSubHeading,
|
|
||||||
Label,
|
Label,
|
||||||
|
List,
|
||||||
|
ShellSubHeading,
|
||||||
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
showToast,
|
showToast,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Calendar } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
@@ -267,7 +266,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={Calendar}
|
Icon="calendar"
|
||||||
headline={t("no_category_apps", {
|
headline={t("no_category_apps", {
|
||||||
category: t("calendar").toLowerCase(),
|
category: t("calendar").toLowerCase(),
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loaded 100 of 595 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user