Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87d2ec9e75 | ||
|
|
226686d4dd | ||
|
|
518ab82e3b | ||
|
|
532b78a9b3 | ||
|
|
1251091300 | ||
|
|
ce1459011a | ||
|
|
e1bdac077a | ||
|
|
d666f2926c | ||
|
|
745f0b0872 | ||
|
|
65f50e9f25 | ||
|
|
54162f8568 | ||
|
|
0f4b9b2a6e | ||
|
|
70b4e3a9c6 | ||
|
|
ab44d5b92c | ||
|
|
bed08c1e4f | ||
|
|
1e0f7f433f | ||
|
|
0cc7a9fc22 | ||
|
|
b03b878199 | ||
|
|
7a560cf09b | ||
|
|
58ad441223 | ||
|
|
d8de919acb | ||
|
|
d699b33dcb | ||
|
|
c071df4cd6 | ||
|
|
219b13d053 | ||
|
|
5e09281974 | ||
|
|
b66d36a98e | ||
|
|
68ce9524eb | ||
|
|
357a42b512 | ||
|
|
33d7da88bf | ||
|
|
cd888be61e | ||
|
|
06231ec299 | ||
|
|
e38705c3e8 | ||
|
|
5c95268d1a | ||
|
|
b310eb1a7e | ||
|
|
051510fa36 | ||
|
|
7d1226b823 | ||
|
|
7732a22c49 | ||
|
|
2a41cd0770 | ||
|
|
a574e5e60f | ||
|
|
0b6508379b | ||
|
|
f6930df87d | ||
|
|
7d4e8e2210 | ||
|
|
50eeb712e3 | ||
|
|
75edc4539c | ||
|
|
def87ea84a | ||
|
|
4153e5b2d5 | ||
|
|
8c2f6ae145 | ||
|
|
78ea4d2f68 | ||
|
|
e7c638c1e4 | ||
|
|
c28fab4fe2 | ||
|
|
e8a02554b6 | ||
|
|
4017c0e28f | ||
|
|
d72f7f8a42 | ||
|
|
3a4ebadbf3 | ||
|
|
765d601ff9 | ||
|
|
3a22fdb428 | ||
|
|
93580ac4bc | ||
|
|
b15ad6f233 | ||
|
|
56e59ab3e1 | ||
|
|
ee796933ee | ||
|
|
d2f76edfd6 | ||
|
|
822f0204a6 | ||
|
|
9770aff153 | ||
|
|
723d0ddec2 | ||
|
|
60b61eb625 | ||
|
|
5d2d99006b | ||
|
|
5d01eb2f38 | ||
|
|
ac3fb0a1cd | ||
|
|
1730ef7d5d | ||
|
|
9e371ded90 | ||
|
|
15098bdd1e | ||
|
|
625a7ec180 | ||
|
|
5602f7bd95 | ||
|
|
3e2caa9e1e | ||
|
|
b905de435d | ||
|
|
23bb06cc36 | ||
|
|
dd24100035 | ||
|
|
7434d91d48 | ||
|
|
3b1de34451 | ||
|
|
4cf89d273e | ||
|
|
e58ce4af71 | ||
|
|
15122dd5e9 | ||
|
|
793c0c60e3 | ||
|
|
344454e574 | ||
|
|
86342d103a | ||
|
|
d4f3726875 | ||
|
|
817a3decb7 | ||
|
|
5f082e11f8 | ||
|
|
5fb5081faa | ||
|
|
17f328fc5e | ||
|
|
d19034c2d5 | ||
|
|
4cdcc85620 | ||
|
|
3f51ae626c | ||
|
|
8499aebe88 | ||
|
|
07f613050e | ||
|
|
2c8c8368a0 | ||
|
|
74d3a84e27 | ||
|
|
33c1d49c52 | ||
|
|
889ff177c1 | ||
|
|
d8a6c375d0 | ||
|
|
655127d155 | ||
|
|
17c4a10e33 | ||
|
|
30f9f19a9f | ||
|
|
be7d45afe2 | ||
|
|
d2bec46d89 | ||
|
|
b90500778e | ||
|
|
bddc235c3c | ||
|
|
360b729d08 | ||
|
|
5d325495a9 | ||
|
|
dead2b843a | ||
|
|
1174cb0d8e | ||
|
|
58e6f18594 | ||
|
|
ffe6a46e66 | ||
|
|
37c02572de | ||
|
|
b82b0a33a4 | ||
|
|
b1bbbcee90 | ||
|
|
1a381695e5 | ||
|
|
8831ff13c7 | ||
|
|
64845c670d | ||
|
|
ba06c6c26a | ||
|
|
49df99cf92 | ||
|
|
0cb2e60d9b | ||
|
|
b3485b07bf | ||
|
|
01933e5c15 | ||
|
|
5f5cf92456 | ||
|
|
29ccc58806 | ||
|
|
26bd02bd94 | ||
|
|
04bf23ed69 | ||
|
|
a7b5fd593e | ||
|
|
6dd4933337 | ||
|
|
4217efc5a8 | ||
|
|
aca2b29aa4 | ||
|
|
a18807dbc2 | ||
|
|
bd1664ad6e | ||
|
|
23cbd838c2 | ||
|
|
a4a1c016dc | ||
|
|
1b176f0d49 | ||
|
|
99d4461dcc | ||
|
|
7d7e0ae6a7 | ||
|
|
c3638eb990 | ||
|
|
cfc157b665 | ||
|
|
96bf81ce8a | ||
|
|
0b9133c2cf | ||
|
|
bd6c471652 | ||
|
|
5ee3744015 | ||
|
|
98235d00c8 | ||
|
|
5c2d2deb82 | ||
|
|
36fa8854ad | ||
|
|
56022a1ba8 | ||
|
|
b67d37fac7 | ||
|
|
615a89737d | ||
|
|
d72c814999 | ||
|
|
124471ef4a | ||
|
|
96e93138e7 | ||
|
|
f28c2645db | ||
|
|
650399ddc7 | ||
|
|
64ad1c8d5b | ||
|
|
2714368a90 | ||
|
|
9bcd1d3609 | ||
|
|
b3fab3aacd | ||
|
|
e3741f3e06 | ||
|
|
ae4e0f8893 | ||
|
|
9c64e8dc28 | ||
|
|
bbb86f2174 | ||
|
|
a559daa076 | ||
|
|
e723b989a8 | ||
|
|
490540ac56 | ||
|
|
c25c835b76 | ||
|
|
ca05e701a8 | ||
|
|
4541f0d714 | ||
|
|
0ec5153f14 | ||
|
|
90ce0f8d4d | ||
|
|
5e9902921f | ||
|
|
5d3c72271b | ||
|
|
775010cd2d | ||
|
|
87a8b5a8dd | ||
|
|
973b624882 | ||
|
|
0a0c5ef61e | ||
|
|
46f67bba9b | ||
|
|
d9806da049 | ||
|
|
42fc49488d | ||
|
|
e030fe0e07 | ||
|
|
51428087ef | ||
|
|
3942df369d | ||
|
|
1d309a6f09 | ||
|
|
56f31eee7a | ||
|
|
2dae0bfb8c | ||
|
|
917c7b0764 | ||
|
|
fd64b0f468 | ||
|
|
b1909337db | ||
|
|
039bf6d4e3 | ||
|
|
83e15c4ce1 | ||
|
|
7d6ecf7201 | ||
|
|
bc42c27230 | ||
|
|
57abb26c31 | ||
|
|
a136511c8a | ||
|
|
c65dca9486 |
@@ -19,6 +19,11 @@
|
||||
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||
# @see https://console.cal.com
|
||||
CALCOM_LICENSE_KEY=
|
||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||
CAL_SIGNATURE_TOKEN=
|
||||
# The route to the Cal.com License API
|
||||
CALCOM_PRIVATE_API_ROUTE=
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
@@ -359,3 +364,6 @@ UNKEY_ROOT_KEY=
|
||||
# Used for Cal.ai Enterprise Voice AI Agents
|
||||
# https://retellai.com
|
||||
RETELL_AI_KEY=
|
||||
|
||||
# Used to disallow emails as being added as guests on bookings
|
||||
BLACKLISTED_GUEST_EMAILS=
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
node_modules
|
||||
apps/api/v2/dist
|
||||
packages/platform/**/dist/*
|
||||
**/**/node_modules
|
||||
**/**/.next
|
||||
**/**/public
|
||||
|
||||
@@ -8,7 +8,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache production build
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-build
|
||||
env:
|
||||
# WARN: Don't touch this cache key. Currently github.sha refers to the latest commit in main
|
||||
|
||||
@@ -12,7 +12,7 @@ runs:
|
||||
steps:
|
||||
- name: Cache database
|
||||
id: cache-db
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
env:
|
||||
cache-name: cache-db
|
||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||
|
||||
@@ -4,7 +4,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -20,7 +20,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Use Node ${{ inputs.node_version }}
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
- name: Expose yarn config as "$GITHUB_OUTPUT"
|
||||
@@ -32,7 +32,7 @@ runs:
|
||||
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
|
||||
# Yarn cache is also reusable between arch and os.
|
||||
- name: Restore yarn cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
id: yarn-download-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||
@@ -43,7 +43,7 @@ runs:
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore node_modules
|
||||
id: yarn-nm-cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: "**/node_modules/"
|
||||
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
@@ -51,7 +51,7 @@ runs:
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore yarn install state
|
||||
id: yarn-install-state-cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: .yarn/ci-cache/
|
||||
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- name: Cache playwright binaries
|
||||
id: playwright-cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
|
||||
+7
-3
@@ -1,6 +1,10 @@
|
||||
"❗️ migrations":
|
||||
- packages/prisma/migrations/**/migration.sql
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- packages/prisma/migrations/**/migration.sql
|
||||
|
||||
"❗️ .env changes":
|
||||
- .env.example
|
||||
- .env.appStore.example
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .env.example
|
||||
- .env.appStore.example
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
name: All checks
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
type-check:
|
||||
name: Type check
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
unit-test:
|
||||
name: Tests
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v1:
|
||||
name: Production builds
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v2:
|
||||
name: Production builds
|
||||
uses: ./.github/workflows/api-v2-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
integration-test:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/integration-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
name: Analyze Build
|
||||
needs: [build]
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
@@ -60,11 +60,31 @@ jobs:
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- name: Cache API v1 production build
|
||||
uses: buildjet/cache@v3
|
||||
id: cache-api-v1-build
|
||||
env:
|
||||
cache-name: api-v1-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/api/v1/**.[jt]s', 'apps/api/v1/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
# Ensures production-build.yml will always be fresh
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/api/v1/.next
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
if [ ${{ steps.cache-api-v1-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v1 build. Skipping build."
|
||||
else
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -30,13 +30,31 @@ jobs:
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Cache API v2 production build
|
||||
uses: buildjet/cache@v3
|
||||
id: cache-api-v2-build
|
||||
env:
|
||||
cache-name: api-v2-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/api/v2/**.[jt]s', 'apps/api/v2/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
# Ensures production-build.yml will always be fresh
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
if [ ${{ steps.cache-api-v2-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v2 build. Skipping build."
|
||||
else
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
check-types:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Show info
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
run: |
|
||||
LAST_DAY=$(date -d tomorrow +%d)
|
||||
if [ "$LAST_DAY" == "01" ]; then
|
||||
echo "::set-output name=is_last_day::true"
|
||||
echo "is_last_day=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::set-output name=is_last_day::false"
|
||||
echo "is_last_day=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: cURL request
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- uses: buildjet/cache-delete@v1
|
||||
with:
|
||||
cache_key: ${{ inputs.cache_key }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: E2E App-Store Apps Tests
|
||||
name: E2E App Store Tests
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
@@ -41,7 +41,7 @@ env:
|
||||
jobs:
|
||||
e2e-app-store:
|
||||
timeout-minutes: 20
|
||||
name: E2E App Store (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
name: E2E App Store
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
@@ -70,16 +70,13 @@ jobs:
|
||||
- 1025:1025
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
## There aren't many tests for AppStore. So, just start with 2 shards. Increase if needed.
|
||||
shard: [1, 2]
|
||||
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
@@ -93,10 +90,10 @@ jobs:
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
run: yarn e2e:app-store
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
name: app-store-results
|
||||
path: test-results
|
||||
|
||||
@@ -41,7 +41,7 @@ env:
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
name: E2E Embed React (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
name: E2E Embed React
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
@@ -62,16 +62,13 @@ jobs:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
## There aren't many tests for embed-react. So, just start with 2 shards. Increase if needed.
|
||||
shard: [1, 2]
|
||||
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
@@ -79,11 +76,11 @@ jobs:
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: |
|
||||
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
yarn e2e:embed-react
|
||||
yarn workspace @calcom/embed-react packaged:tests
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
name: embed-react-results
|
||||
path: test-results
|
||||
|
||||
@@ -41,7 +41,7 @@ env:
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
name: E2E Embed Core (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
name: E2E Embed Core
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
@@ -70,26 +70,23 @@ jobs:
|
||||
- 1025:1025
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
## There aren't many tests for embed-core. So, just start with 2 shards. Increase if needed.
|
||||
shard: [1, 2]
|
||||
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
run: yarn e2e:embed
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
name: embed-core-results
|
||||
path: test-results
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: E2E tests
|
||||
name: E2E
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
@@ -41,7 +41,7 @@ env:
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
name: E2E tests (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
name: E2E (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
@@ -71,13 +71,13 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7]
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Integration
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_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 }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
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 }}
|
||||
jobs:
|
||||
integration:
|
||||
timeout-minutes: 20
|
||||
name: Integration
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
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
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- name: Run Tests
|
||||
run: yarn test -- --integrationTestsOnly
|
||||
# TODO: Generate test results so we can upload them
|
||||
# - name: Upload Test Results
|
||||
# if: ${{ always() }}
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: test-results
|
||||
# path: test-results
|
||||
@@ -12,18 +12,16 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
team-labels:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
@@ -39,7 +37,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -6,7 +6,7 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Run Linting with Reports
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -20,9 +19,9 @@ jobs:
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -35,13 +34,6 @@ jobs:
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
needs: [changes]
|
||||
@@ -49,64 +41,22 @@ jobs:
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v1:
|
||||
name: Production builds
|
||||
unit-test:
|
||||
name: Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v2:
|
||||
name: Production builds
|
||||
integration-test:
|
||||
name: Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v2-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
name: Analyze Build
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
uses: ./.github/workflows/integration-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [changes, lint, type-check, test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [changes, lint, type-check, unit-test, integration-test]
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Determine tag"
|
||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main # Always checkout main even for tagged releases
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -37,12 +37,3 @@ jobs:
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Unit tests
|
||||
name: Unit
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_run:
|
||||
@@ -6,10 +6,11 @@ on:
|
||||
types: [completed]
|
||||
jobs:
|
||||
test:
|
||||
name: Unit
|
||||
timeout-minutes: 20
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ To develop locally:
|
||||
|
||||
- Duplicate `.env.example` to `.env`.
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
|
||||
6. Setup Node
|
||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
// reduce sample rate to 10% on production
|
||||
tracesSampleRate: process.env.NODE_ENV !== "production" ? 1.0 : 0.1,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { isAdminGuard } from "../utils/isAdmin";
|
||||
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
||||
|
||||
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
||||
export const dateNotInPast = function (date: Date) {
|
||||
@@ -36,7 +37,10 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
|
||||
// save the user id in the request for later use
|
||||
req.userId = apiKey.userId;
|
||||
// save the isAdmin boolean here for later use
|
||||
req.isAdmin = await isAdminGuard(req);
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
|
||||
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
|
||||
|
||||
await next();
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { NextMiddleware } from "next-api-middleware";
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
export const verifyCredentialSyncEnabled: NextMiddleware = async (req, res, next) => {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
return res.status(403).json({ error: "Only admin API keys can access credential syncing endpoints" });
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
export function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
||||
export function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
|
||||
@@ -1,10 +1,37 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { ScopeOfAdmin } from "./scopeOfAdmin";
|
||||
|
||||
export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
const { userId } = req;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
return user?.role === UserPermissionRole.ADMIN;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
|
||||
if (!user) return { isAdmin: false, scope: null };
|
||||
|
||||
const { role: userRole } = user;
|
||||
if (userRole === UserPermissionRole.ADMIN) return { isAdmin: true, scope: ScopeOfAdmin.SystemWide };
|
||||
|
||||
const orgOwnerOrAdminMemberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
|
||||
|
||||
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
type AccessibleUsersType = {
|
||||
memberUserIds: number[];
|
||||
adminUserId: number;
|
||||
};
|
||||
|
||||
const getAllOrganizationMemberships = async (
|
||||
memberships: {
|
||||
userId: number;
|
||||
role: MembershipRole;
|
||||
teamId: number;
|
||||
}[],
|
||||
orgId: number
|
||||
) => {
|
||||
return memberships.reduce<number[]>((acc, membership) => {
|
||||
if (membership.teamId === orgId) {
|
||||
acc.push(membership.userId);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const getAllAdminMemberships = async (userId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getAllOrganizationMembers = async (organizationId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: organizationId,
|
||||
accepted: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAccessibleUsers = async ({
|
||||
memberUserIds,
|
||||
adminUserId,
|
||||
}: AccessibleUsersType): Promise<number[]> => {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
accepted: true,
|
||||
OR: [
|
||||
{ userId: { in: memberUserIds } },
|
||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
role: true,
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgId = memberships.find((membership) => membership.userId === adminUserId)?.teamId;
|
||||
if (!orgId) return [];
|
||||
|
||||
const allAccessibleMemberUserIds = await getAllOrganizationMemberships(memberships, orgId);
|
||||
const accessibleUserIds = allAccessibleMemberUserIds.filter((userId) => userId !== adminUserId);
|
||||
return accessibleUserIds;
|
||||
};
|
||||
|
||||
export const retrieveOrgScopedAccessibleUsers = async ({ adminId }: { adminId: number }) => {
|
||||
const adminMemberships = await getAllAdminMemberships(adminId);
|
||||
const organizationId = adminMemberships.find((membership) => membership.team.isOrganization)?.team.id;
|
||||
if (!organizationId) return [];
|
||||
|
||||
const allMemberships = await getAllOrganizationMembers(organizationId);
|
||||
return allMemberships.map((membership) => membership.userId);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const ScopeOfAdmin = {
|
||||
SystemWide: "SystemWide",
|
||||
OrgOwnerOrAdmin: "OrgOwnerOrAdmin",
|
||||
} as const;
|
||||
@@ -55,7 +55,7 @@ export const schemaBookingReadPublic = Booking.extend({
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
})
|
||||
.optional(),
|
||||
.nullish(),
|
||||
payment: z
|
||||
.array(
|
||||
_PaymentModel.pick({
|
||||
|
||||
@@ -14,3 +14,7 @@ export const withValidQueryIdTransformParseInt = withValidation({
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
|
||||
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
|
||||
recordingId: z.string(),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@ const { withAxiom } = require("next-axiom");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
const plugins = [withAxiom];
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
instrumentationHook: true,
|
||||
},
|
||||
transpilePackages: [
|
||||
"@calcom/app-store",
|
||||
"@calcom/core",
|
||||
@@ -87,12 +92,12 @@ const nextConfig = {
|
||||
};
|
||||
|
||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
nextConfig["sentry"] = {
|
||||
autoInstrumentServerFunctions: true,
|
||||
hideSourceMaps: true,
|
||||
};
|
||||
|
||||
plugins.push(withSentryConfig);
|
||||
plugins.push((nextConfig) =>
|
||||
withSentryConfig(nextConfig, {
|
||||
autoInstrumentServerFunctions: true,
|
||||
hideSourceMaps: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
||||
Vendored
+2
-1
@@ -11,7 +11,8 @@ export declare module "next" {
|
||||
method: string;
|
||||
// session: { user: { id: number } };
|
||||
// query: Partial<{ [key: string]: string | string[] }>;
|
||||
isAdmin: boolean;
|
||||
isSystemWideAdmin: boolean;
|
||||
isOrganizationOwnerOrAdmin: boolean;
|
||||
pagination: { take: number; skip: number };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@sentry/nextjs": "^7.73.0",
|
||||
"@sentry/nextjs": "^8.8.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "^13.5.4",
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
export async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
// Admin can check any api key
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if user can access the api key
|
||||
const apiKey = await prisma.apiKey.findFirst({
|
||||
where: { id, userId },
|
||||
|
||||
@@ -16,8 +16,8 @@ type CustomNextApiRequest = NextApiRequest & {
|
||||
function handleAdminRequests(req: CustomNextApiRequest) {
|
||||
// To match type safety with runtime
|
||||
if (!hasReqArgs(req)) throw Error("Missing req.args");
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin && req.query.userId) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
req.args.where = { userId: { in: userIds } };
|
||||
@@ -30,8 +30,8 @@ function hasReqArgs(req: CustomNextApiRequest): req is Ensure<CustomNextApiReque
|
||||
}
|
||||
|
||||
async function getHandler(req: CustomNextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
req.args = isAdmin ? {} : { where: { userId } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
req.args = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
// Proof of concept: allowing mutation in exchange of composability
|
||||
handleAdminRequests(req);
|
||||
const data = await prisma.apiKey.findMany(req.args);
|
||||
|
||||
@@ -10,7 +10,7 @@ import prisma from "@calcom/prisma";
|
||||
import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
||||
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
||||
const args: Prisma.ApiKeyCreateArgs = {
|
||||
@@ -24,9 +24,10 @@ async function postHandler(req: NextApiRequest) {
|
||||
},
|
||||
};
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const query = schemaQueryIdParseInt.parse(req.query);
|
||||
// @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin.
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all user bookings, including attendees
|
||||
const attendee = await prisma.attendee.findFirst({
|
||||
where: { id: query.id, booking: { userId } },
|
||||
|
||||
@@ -63,8 +63,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaAttendeeEditBodyParams>) {
|
||||
const { isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const { userId } = req;
|
||||
const { bookingId } = body;
|
||||
if (bookingId) {
|
||||
|
||||
@@ -31,8 +31,8 @@ import { schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
* description: No attendees were found
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.AttendeeFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.AttendeeFindManyArgs = isSystemWideAdmin ? {} : { where: { booking: { userId } } };
|
||||
const data = await prisma.attendee.findMany(args);
|
||||
const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee));
|
||||
if (!attendees) throw new HttpError({ statusCode: 404, message: "No attendees were found" });
|
||||
|
||||
@@ -52,10 +52,10 @@ import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "~/lib/
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaAttendeeCreateBodyParams.parse(req.body);
|
||||
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
const userBooking = await prisma.booking.findFirst({
|
||||
where: { userId, id: body.bookingId },
|
||||
select: { id: true },
|
||||
|
||||
@@ -5,10 +5,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin, query } = req;
|
||||
const { userId, isSystemWideAdmin, query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
/** Admins can skip the ownership verification */
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
/**
|
||||
* There's a caveat here. If the availability exists but the user doesn't own it,
|
||||
* the user will see a 404 error which may or not be the desired behavior.
|
||||
|
||||
@@ -86,8 +86,8 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
||||
const schedule = await prisma.schedule.findFirst({
|
||||
where: { userId, id: data.scheduleId },
|
||||
|
||||
@@ -189,7 +189,7 @@ const availabilitySchema = z
|
||||
);
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { isAdmin, userId: reqUserId } = req;
|
||||
const { isSystemWideAdmin, userId: reqUserId } = req;
|
||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||
if (!teamId)
|
||||
return getUserAvailability({
|
||||
@@ -224,7 +224,7 @@ async function handler(req: NextApiRequest) {
|
||||
const isUserAdminOrOwner =
|
||||
memberRoles[reqUserId] == MembershipRole.ADMIN ||
|
||||
memberRoles[reqUserId] == MembershipRole.OWNER ||
|
||||
isAdmin;
|
||||
isSystemWideAdmin;
|
||||
if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
const availabilities = members.map(async (user) => {
|
||||
return {
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Here we make sure to only return references of the user's own bookings if the user is not an admin.
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all references where the user has bookings
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { id, booking: { userId } },
|
||||
|
||||
@@ -60,12 +60,12 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body, isAdmin, userId } = req;
|
||||
const { query, body, isSystemWideAdmin, userId } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaBookingEditBodyParams.parse(body);
|
||||
/* If user tries to update bookingId, we run extra checks */
|
||||
if (data.bookingId) {
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: data.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
|
||||
@@ -30,8 +30,10 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe
|
||||
* description: No booking references were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { booking: { userId } } };
|
||||
const data = await prisma.bookingReference.findMany(args);
|
||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaBookingCreateBodyParams.parse(req.body);
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: body.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
|
||||
@@ -3,15 +3,33 @@ import type { NextApiRequest } from "next";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin, query } = req;
|
||||
if (isAdmin) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin, query } = req;
|
||||
if (isSystemWideAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
if (isOrganizationOwnerOrAdmin) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (booking) {
|
||||
const bookingUserId = booking.userId;
|
||||
if (bookingUserId) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [bookingUserId],
|
||||
});
|
||||
if (accessibleUsersIds.length > 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
@@ -43,7 +61,7 @@ async function authMiddleware(req: NextApiRequest) {
|
||||
});
|
||||
|
||||
if (!teamBookings) {
|
||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
@@ -109,14 +110,27 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
|
||||
const { isAdmin } = req;
|
||||
if (body.userId && !isAdmin) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||
if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) {
|
||||
// Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
|
||||
if (body.userId && isOrganizationOwnerOrAdmin) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [body.userId],
|
||||
});
|
||||
if (accessibleUsersIds.length === 0) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getRecordingsOfCalVideoByRoomName } from "@calcom/core/videoClient";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/recordings:
|
||||
* get:
|
||||
* summary: Find all Cal video recordings of that booking
|
||||
* operationId: getRecordingsByBookingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which recordings need to be fetched. Recording download link is only valid for 12 hours and you would have to fetch the recordings again to get new download link
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/ArrayOfRecordings"
|
||||
* examples:
|
||||
* recordings:
|
||||
* value:
|
||||
* - id: "ad90a2e7-154f-49ff-a815-5da1db7bf899"
|
||||
* room_name: "0n22w24AQ5ZFOtEKX2gX"
|
||||
* start_ts: 1716215386
|
||||
* status: "finished"
|
||||
* max_participants: 1
|
||||
* duration: 11
|
||||
* share_token: "x94YK-69Gnh7"
|
||||
* download_link: "https://daily-meeting-recordings..."
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${id}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Cal Video reference found with booking id ${booking.id}`,
|
||||
});
|
||||
|
||||
const recordings = await getRecordingsOfCalVideoByRoomName(roomName);
|
||||
|
||||
if (!recordings || !("data" in recordings)) return [];
|
||||
|
||||
const recordingWithDownloadLink = recordings.data.map((recording: RecordingItemSchema) => {
|
||||
return getDownloadLinkOfCalVideoByRecordingId(recording.id)
|
||||
.then((res) => ({
|
||||
...recording,
|
||||
download_link: res?.download_link,
|
||||
}))
|
||||
.catch((err) => ({ ...recording, download_link: null, error: err.message }));
|
||||
});
|
||||
const res = await Promise.all(recordingWithDownloadLink);
|
||||
return res;
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import {
|
||||
getTranscriptsAccessLinkFromRecordingId,
|
||||
checkIfRoomNameMatchesInRecording,
|
||||
} from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { getTranscriptFromRecordingId } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/transcripts/{recordingId}:
|
||||
* get:
|
||||
* summary: Find all Cal video transcripts of that recording
|
||||
* operationId: getTranscriptsByRecordingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which transcripts need to be fetched.
|
||||
* - in: path
|
||||
* name: recordingId
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: ID of the recording(daily.co recording id) for which transcripts need to be fetched.
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id, recordingId } = getTranscriptFromRecordingId.parse(query);
|
||||
|
||||
await checkIfRecordingBelongsToBooking(id, recordingId);
|
||||
|
||||
const transcriptsAccessLinks = await getTranscriptsAccessLinkFromRecordingId(recordingId);
|
||||
|
||||
return transcriptsAccessLinks;
|
||||
}
|
||||
|
||||
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${bookingId}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking Reference with Daily Video found with booking id ${bookingId}`,
|
||||
});
|
||||
|
||||
const canUserAccessRecordingId = await checkIfRoomNameMatchesInRecording(roomName, recordingId);
|
||||
if (!canUserAccessRecordingId) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: `This Recording Id ${recordingId} does not belong to booking ${bookingId}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/transcripts:
|
||||
* get:
|
||||
* summary: Find all Cal video transcripts of that booking
|
||||
* operationId: getTranscriptsByBookingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which recordings need to be fetched.
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${id}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Cal Video reference found with booking id ${booking.id}`,
|
||||
});
|
||||
|
||||
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(roomName);
|
||||
|
||||
return transcripts;
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -5,6 +5,11 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import {
|
||||
getAccessibleUsers,
|
||||
retrieveOrgScopedAccessibleUsers,
|
||||
} from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
@@ -108,7 +113,11 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
* 404:
|
||||
* description: No bookings were found
|
||||
*/
|
||||
|
||||
type GetAdminArgsType = {
|
||||
adminDidQueryUserIds?: boolean;
|
||||
requestedUserIds: number[];
|
||||
userId: number;
|
||||
};
|
||||
/**
|
||||
* Constructs the WHERE clause for Prisma booking findMany operation.
|
||||
*
|
||||
@@ -161,12 +170,20 @@ function buildWhereClause(
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
|
||||
export async function handler(req: NextApiRequest) {
|
||||
const {
|
||||
userId,
|
||||
isSystemWideAdmin,
|
||||
isOrganizationOwnerOrAdmin,
|
||||
pagination: { take, skip },
|
||||
} = req;
|
||||
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
|
||||
|
||||
const args: Prisma.BookingFindManyArgs = {};
|
||||
if (req.query.take && req.query.page) {
|
||||
args.take = take;
|
||||
args.skip = skip;
|
||||
}
|
||||
args.include = {
|
||||
attendees: true,
|
||||
user: true,
|
||||
@@ -182,19 +199,32 @@ async function handler(req: NextApiRequest) {
|
||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (isAdmin) {
|
||||
if (req.query.userId) {
|
||||
if (isSystemWideAdmin) {
|
||||
if (req.query.userId || filterByAttendeeEmails) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails);
|
||||
} else if (filterByAttendeeEmails) {
|
||||
args.where = buildWhereClause(null, attendeeEmails, [], []);
|
||||
const requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
|
||||
const systemWideAdminArgs = {
|
||||
adminDidQueryUserIds: !!req.query.userId,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
};
|
||||
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
||||
}
|
||||
} else if (isOrganizationOwnerOrAdmin) {
|
||||
let requestedUserIds = [userId];
|
||||
if (req.query.userId || filterByAttendeeEmails) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
}
|
||||
const orgWideAdminArgs = {
|
||||
adminDidQueryUserIds: !!req.query.userId,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
};
|
||||
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
|
||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
||||
} else {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
@@ -203,7 +233,7 @@ async function handler(req: NextApiRequest) {
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new HttpError({ message: "User not found", statusCode: 500 });
|
||||
throw new HttpError({ message: "User not found", statusCode: 404 });
|
||||
}
|
||||
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||
}
|
||||
@@ -225,4 +255,53 @@ async function handler(req: NextApiRequest) {
|
||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
const handleSystemWideAdminArgs = async ({
|
||||
adminDidQueryUserIds,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
}: GetAdminArgsType) => {
|
||||
if (adminDidQueryUserIds) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: requestedUserIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
|
||||
return { userId, userIds: requestedUserIds, userEmails };
|
||||
}
|
||||
return { userId: null, userIds: [], userEmails: [] };
|
||||
};
|
||||
|
||||
const handleOrgWideAdminArgs = async ({
|
||||
adminDidQueryUserIds,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
}: GetAdminArgsType) => {
|
||||
if (adminDidQueryUserIds) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: requestedUserIds,
|
||||
});
|
||||
|
||||
if (!accessibleUsersIds.length) throw new HttpError({ message: "No User found", statusCode: 404 });
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: accessibleUsersIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
return { userId, userIds: accessibleUsersIds, userEmails };
|
||||
} else {
|
||||
const accessibleUsersIds = await retrieveOrgScopedAccessibleUsers({
|
||||
adminId: userId,
|
||||
});
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: accessibleUsersIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
return { userId, userIds: accessibleUsersIds, userEmails };
|
||||
}
|
||||
};
|
||||
|
||||
export default withMiddleware("pagination")(defaultResponder(handler));
|
||||
|
||||
@@ -2,8 +2,12 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
@@ -204,10 +208,28 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) req.userId = req.body.userId || userId;
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||
if (isSystemWideAdmin) req.userId = req.body.userId || userId;
|
||||
|
||||
return await handleNewBooking(req, getBookingDataSchemaForApi);
|
||||
if (isOrganizationOwnerOrAdmin) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [req.body.userId || userId],
|
||||
});
|
||||
const [requestedUserId] = accessibleUsersIds;
|
||||
req.userId = requestedUserId || userId;
|
||||
}
|
||||
|
||||
try {
|
||||
return await handleNewBooking(req, getBookingDataSchemaForApi);
|
||||
} catch (error: unknown) {
|
||||
const knownError = error as Error;
|
||||
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
|
||||
throw new HttpError({ statusCode: 400, message: knownError.message });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
|
||||
@@ -97,9 +97,10 @@ import { schemaConnectedCalendarsReadPublic } from "~/lib/validations/connected-
|
||||
*/
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
|
||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user can access the event type of this input
|
||||
const eventTypeCustomInput = await prisma.eventTypeCustomInput.findFirst({
|
||||
where: { id, eventType: { userId } },
|
||||
|
||||
@@ -29,8 +29,10 @@ import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-c
|
||||
* description: No eventTypeCustomInputs were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.EventTypeCustomInputFindManyArgs = isAdmin ? {} : { where: { eventType: { userId } } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.EventTypeCustomInputFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { eventType: { userId } } };
|
||||
const data = await prisma.eventTypeCustomInput.findMany(args);
|
||||
return { event_type_custom_inputs: data.map((v) => schemaEventTypeCustomInputPublic.parse(v)) };
|
||||
}
|
||||
|
||||
@@ -80,10 +80,10 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { eventTypeId, ...body } = schemaEventTypeCustomInputBodyParams.parse(req.body);
|
||||
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
/* We check that the user has access to the event type he's trying to add a custom input to. */
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: { id: eventTypeId, userId },
|
||||
|
||||
@@ -6,9 +6,9 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
const userEventTypes = await prisma.eventType.findMany({
|
||||
where: { userId },
|
||||
select: { id: true },
|
||||
|
||||
@@ -83,10 +83,10 @@ type UserCredentialType = {
|
||||
};
|
||||
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, query, body } = req;
|
||||
const { userId, isSystemWideAdmin, query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||
const assignedUserId = isSystemWideAdmin ? parsedBody.userId || userId : userId;
|
||||
|
||||
validateIntegrationInput(parsedBody);
|
||||
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||
|
||||
@@ -61,11 +61,11 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, body } = req;
|
||||
const { userId, isSystemWideAdmin, body } = req;
|
||||
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||
await checkPermissions(req, userId);
|
||||
|
||||
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||
const assignedUserId = isSystemWideAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||
|
||||
/* Check if credentialId data matches the ownership and integration passed in */
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
@@ -120,19 +120,20 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, userId: number) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
const body = schemaDestinationCalendarCreateBodyParams.parse(req.body);
|
||||
|
||||
/* Non-admin users can only create destination calendars for themselves */
|
||||
if (!isAdmin && body.userId)
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId */
|
||||
if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
||||
if (isSystemWideAdmin && !body.userId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
||||
/* User should only be able to create for their own destination calendars*/
|
||||
if (!isAdmin && body.eventTypeId) {
|
||||
if (!isSystemWideAdmin && body.eventTypeId) {
|
||||
const ownsEventType = await prisma.eventType.findFirst({ where: { id: body.eventTypeId, userId } });
|
||||
if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
}
|
||||
|
||||
@@ -27,6 +27,37 @@ const swaggerHandler = withSwagger({
|
||||
$ref: "#/components/schemas/Booking",
|
||||
},
|
||||
},
|
||||
ArrayOfRecordings: {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: "#/components/schemas/Recording",
|
||||
},
|
||||
},
|
||||
Recording: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
room_name: {
|
||||
type: "string",
|
||||
},
|
||||
start_ts: {
|
||||
type: "number",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
},
|
||||
max_participants: {
|
||||
type: "number",
|
||||
},
|
||||
duration: {
|
||||
type: "number",
|
||||
},
|
||||
download_link: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
Booking: {
|
||||
properties: {
|
||||
id: {
|
||||
|
||||
@@ -46,9 +46,9 @@ export async function deleteHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
/** Only event type owners can delete it */
|
||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -87,7 +87,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
req: NextApiRequest,
|
||||
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
||||
) {
|
||||
if (req.isAdmin) return true;
|
||||
if (req.isSystemWideAdmin) return true;
|
||||
if (eventType?.teamId) {
|
||||
req.query.teamId = String(eventType.teamId);
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
|
||||
@@ -237,9 +237,9 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
/** Only event type owners can modify it */
|
||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -43,10 +43,10 @@ import getCalLink from "./_utils/getCalLink";
|
||||
* description: No event types were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
const { slug } = schemaQuerySlug.parse(req.query);
|
||||
const shouldUseUserId = !isAdmin || !slug || !!req.query.userId;
|
||||
const shouldUseUserId = !isSystemWideAdmin || !slug || !!req.query.userId;
|
||||
// When user is admin and no query params are provided we should return all event types.
|
||||
// But currently we return only the event types of the user. Not changing this for backwards compatibility.
|
||||
const data = await prisma.eventType.findMany({
|
||||
@@ -74,9 +74,9 @@ async function getHandler(req: NextApiRequest) {
|
||||
};
|
||||
}
|
||||
// TODO: Extract & reuse.
|
||||
function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
||||
function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
|
||||
@@ -265,7 +265,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, body } = req;
|
||||
const { userId, isSystemWideAdmin, body } = req;
|
||||
|
||||
const {
|
||||
hosts = [],
|
||||
@@ -291,7 +291,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
await checkUserMembership(req);
|
||||
}
|
||||
|
||||
if (isAdmin && parsedBody.userId) {
|
||||
if (isSystemWideAdmin && parsedBody.userId) {
|
||||
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
||||
}
|
||||
|
||||
@@ -311,18 +311,18 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
const body = schemaEventTypeCreateBodyParams.parse(req.body);
|
||||
/* Non-admin users can only create event types for themselves */
|
||||
if (!isAdmin && body.userId)
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
if (
|
||||
body.teamId &&
|
||||
!isAdmin &&
|
||||
!(await canUserAccessTeamWithRole(req.userId, isAdmin, body.teamId, {
|
||||
!isSystemWideAdmin &&
|
||||
!(await canUserAccessTeamWithRole(req.userId, isSystemWideAdmin, body.teamId, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
}))
|
||||
)
|
||||
@@ -331,7 +331,7 @@ async function checkPermissions(req: NextApiRequest) {
|
||||
message: "ADMIN required for `teamId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId or teamId */
|
||||
if (isAdmin && !body.userId && !body.teamId)
|
||||
if (isSystemWideAdmin && !body.userId && !body.teamId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ export default async function checkTeamEventEditPermission(
|
||||
req: NextApiRequest,
|
||||
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
|
||||
) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
let userId = req.userId;
|
||||
if (isAdmin && body.userId) {
|
||||
if (isSystemWideAdmin && body.userId) {
|
||||
userId = body.userId;
|
||||
}
|
||||
if (body.teamId) {
|
||||
|
||||
@@ -58,10 +58,10 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
||||
if (!isAdmin && "accepted" in body)
|
||||
if (!isSystemWideAdmin && "accepted" in body)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||
// Only team OWNERS and ADMINS can add other members
|
||||
const membership = await prisma.membership.findFirst({
|
||||
|
||||
@@ -7,7 +7,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user";
|
||||
|
||||
async function handler({ userId }: NextApiRequest) {
|
||||
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
||||
return { user: schemaUserReadPublic.parse(data) };
|
||||
return {
|
||||
user: schemaUserReadPublic.parse({
|
||||
...data,
|
||||
avatar: data.avatarUrl,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { membershipIdSchema } from "~/lib/validations/membership";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { teamId } = membershipIdSchema.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Only team members can modify a membership
|
||||
const membership = await prisma.membership.findFirst({ where: { userId, teamId } });
|
||||
if (!membership) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -43,10 +43,10 @@ export async function deleteHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isAdmin, userId, query } = req;
|
||||
const { isSystemWideAdmin, userId, query } = req;
|
||||
const userId_teamId = membershipIdSchema.parse(query);
|
||||
// Admin User can do anything including deletion of Admin Team Member in any team
|
||||
if (isAdmin) {
|
||||
if (isSystemWideAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,11 +52,11 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: queryUserId, teamId } = membershipIdSchema.parse(req.query);
|
||||
const data = membershipEditBodySchema.parse(req.body);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Only the invited user can accept the invite
|
||||
if ("accepted" in data && queryUserId !== userId)
|
||||
throw new HttpError({
|
||||
|
||||
@@ -46,26 +46,28 @@ async function getHandler(req: NextApiRequest) {
|
||||
* Returns requested users IDs only if admin, otherwise return only current user ID
|
||||
*/
|
||||
function getUserIds(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/** Only admins can query other users */
|
||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isAdmin && req.query.userId) {
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
return userIds;
|
||||
}
|
||||
// Return all memberships for ADMIN, limit to current user to non-admins
|
||||
return isAdmin ? undefined : [userId];
|
||||
return isSystemWideAdmin ? undefined : [userId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns requested teams IDs only if admin
|
||||
*/
|
||||
function getTeamIds(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
/** Only admins can query other teams */
|
||||
if (!isAdmin && req.query.teamId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isAdmin && req.query.teamId) {
|
||||
if (!isSystemWideAdmin && req.query.teamId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.teamId) {
|
||||
const query = schemaQuerySingleOrMultipleTeamIds.parse(req.query);
|
||||
const teamIds = Array.isArray(query.teamId) ? query.teamId : [query.teamId];
|
||||
return teamIds;
|
||||
|
||||
@@ -37,11 +37,11 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const body = membershipCreateBodySchema.parse(req.body);
|
||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
||||
if (!isAdmin && "accepted" in body)
|
||||
if (!isSystemWideAdmin && "accepted" in body)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||
// Only team OWNERS and ADMINS can add other members
|
||||
const membership = await prisma.membership.findFirst({
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user can access the schedule
|
||||
const schedule = await prisma.schedule.findFirst({
|
||||
where: { id, userId },
|
||||
|
||||
@@ -91,8 +91,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaSingleScheduleBodyParams>) {
|
||||
const { isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
if (body.userId) {
|
||||
throw new HttpError({ statusCode: 403, message: "Non admin cannot change the owner of a schedule" });
|
||||
}
|
||||
|
||||
@@ -77,17 +77,17 @@ export const schemaUserIds = z
|
||||
*/
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.ScheduleFindManyArgs = isAdmin ? {} : { where: { userId } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.ScheduleFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
args.include = { availability: true };
|
||||
|
||||
if (!isAdmin && req.query.userId)
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "Unauthorized: Only admins can query other users",
|
||||
});
|
||||
|
||||
if (isAdmin && req.query.userId) {
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
args.where = { userId: { in: userIds } };
|
||||
|
||||
@@ -80,14 +80,14 @@ import { schemaCreateScheduleBodyParams, schemaSchedulePublic } from "~/lib/vali
|
||||
*/
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaCreateScheduleBodyParams.parse(req.body);
|
||||
let args: Prisma.ScheduleCreateArgs = { data: { ...body, userId } };
|
||||
|
||||
/* If ADMIN we create the schedule for selected user */
|
||||
if (isAdmin && body.userId) args = { data: { ...body, userId: body.userId } };
|
||||
if (isSystemWideAdmin && body.userId) args = { data: { ...body, userId: body.userId } };
|
||||
|
||||
if (!isAdmin && body.userId)
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required for `userId`" });
|
||||
|
||||
// We create default availabilities for the schedule
|
||||
|
||||
@@ -5,10 +5,10 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: queryUserId } = selectedCalendarIdSchema.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user requesting is the same as the one being requested
|
||||
if (userId !== queryUserId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
@@ -53,14 +53,15 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, isAdmin } = req;
|
||||
const { query, isSystemWideAdmin } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data };
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
|
||||
@@ -32,13 +32,14 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
* description: No selected calendars were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/* Admin gets all selected calendar by default, otherwise only the user's ones */
|
||||
const args: Prisma.SelectedCalendarFindManyArgs = isAdmin ? {} : { where: { userId } };
|
||||
const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isAdmin && req.query.userId) {
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
args.where = { userId: { in: userIds } };
|
||||
|
||||
@@ -50,13 +50,14 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } };
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user