Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d431607125 | ||
|
|
72253021ef | ||
|
|
be0cc31d63 | ||
|
|
38e67ccc79 | ||
|
|
3fe26f19ff | ||
|
|
f38f98a0b6 | ||
|
|
b4d5f511b5 | ||
|
|
86fc5161b6 | ||
|
|
dbf4be27ea | ||
|
|
7b69a6ab9c | ||
|
|
f897c680a4 | ||
|
|
ac05693694 | ||
|
|
3174b55e21 | ||
|
|
08f4a48774 | ||
|
|
928dd49965 | ||
|
|
47a8ba1eaa | ||
|
|
c4e78c213b | ||
|
|
a31cf9963d | ||
|
|
7ff255d1e7 | ||
|
|
4a88051531 | ||
|
|
765b0b44f8 | ||
|
|
10f8b40348 | ||
|
|
c9a9f70801 | ||
|
|
c2d7054ec4 | ||
|
|
89ac9a3d31 | ||
|
|
7d9c7a8c22 | ||
|
|
48e7efccb2 | ||
|
|
0072aba55d | ||
|
|
c7a96e1b43 | ||
|
|
0288ea931f | ||
|
|
c958b6017a | ||
|
|
ffe03be018 | ||
|
|
f760a8ac26 | ||
|
|
d0a7eb12a0 | ||
|
|
9a473d5b55 | ||
|
|
c5a83f4df2 | ||
|
|
3e175dbca3 | ||
|
|
762b995172 | ||
|
|
d2d7453962 | ||
|
|
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 |
No files matched your search
@@ -359,3 +359,6 @@ UNKEY_ROOT_KEY=
|
|||||||
# Used for Cal.ai Enterprise Voice AI Agents
|
# Used for Cal.ai Enterprise Voice AI Agents
|
||||||
# https://retellai.com
|
# https://retellai.com
|
||||||
RETELL_AI_KEY=
|
RETELL_AI_KEY=
|
||||||
|
|
||||||
|
# Used to disallow emails as being added as guests on bookings
|
||||||
|
BLACKLISTED_GUEST_EMAILS=
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
apps/api/v2/dist
|
||||||
|
packages/platform/**/dist/*
|
||||||
**/**/node_modules
|
**/**/node_modules
|
||||||
**/**/.next
|
**/**/.next
|
||||||
**/**/public
|
**/**/public
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Cache production build
|
- name: Cache production build
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
id: cache-build
|
id: cache-build
|
||||||
env:
|
env:
|
||||||
# WARN: Don't touch this cache key. Currently github.sha refers to the latest commit in main
|
# WARN: Don't touch this cache key. Currently github.sha refers to the latest commit in main
|
||||||
@@ -20,13 +20,14 @@ runs:
|
|||||||
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
||||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||||
key-4: ${{ github.sha }}
|
key-4: ${{ github.sha }}
|
||||||
|
key-5: ${{ github.event.pull_request.head.sha }}
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ github.workspace }}/apps/web/.next
|
${{ github.workspace }}/apps/web/.next
|
||||||
${{ github.workspace }}/apps/web/public/embed
|
${{ github.workspace }}/apps/web/public/embed
|
||||||
**/.turbo/**
|
**/.turbo/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}-${{ env.key-5 }}
|
||||||
- run: |
|
- run: |
|
||||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
yarn build
|
yarn build
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Cache database
|
- name: Cache database
|
||||||
id: cache-db
|
id: cache-db
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-db
|
cache-name: cache-db
|
||||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -20,7 +20,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Use Node ${{ inputs.node_version }}
|
- name: Use Node ${{ inputs.node_version }}
|
||||||
uses: buildjet/setup-node@v3
|
uses: buildjet/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node_version }}
|
node-version: ${{ inputs.node_version }}
|
||||||
- name: Expose yarn config as "$GITHUB_OUTPUT"
|
- 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 rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
|
||||||
# Yarn cache is also reusable between arch and os.
|
# Yarn cache is also reusable between arch and os.
|
||||||
- name: Restore yarn cache
|
- name: Restore yarn cache
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
id: yarn-download-cache
|
id: yarn-download-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||||
@@ -43,7 +43,7 @@ runs:
|
|||||||
# Invalidated on yarn.lock changes
|
# Invalidated on yarn.lock changes
|
||||||
- name: Restore node_modules
|
- name: Restore node_modules
|
||||||
id: yarn-nm-cache
|
id: yarn-nm-cache
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
with:
|
with:
|
||||||
path: "**/node_modules/"
|
path: "**/node_modules/"
|
||||||
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||||
@@ -51,7 +51,7 @@ runs:
|
|||||||
# Invalidated on yarn.lock changes
|
# Invalidated on yarn.lock changes
|
||||||
- name: Restore yarn install state
|
- name: Restore yarn install state
|
||||||
id: yarn-install-state-cache
|
id: yarn-install-state-cache
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .yarn/ci-cache/
|
path: .yarn/ci-cache/
|
||||||
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
uses: buildjet/cache@v3
|
uses: buildjet/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/Library/Caches/ms-playwright
|
~/Library/Caches/ms-playwright
|
||||||
|
|||||||
+7
-3
@@ -1,6 +1,10 @@
|
|||||||
"❗️ migrations":
|
"❗️ migrations":
|
||||||
- packages/prisma/migrations/**/migration.sql
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- packages/prisma/migrations/**/migration.sql
|
||||||
|
|
||||||
"❗️ .env changes":
|
"❗️ .env changes":
|
||||||
- .env.example
|
- changed-files:
|
||||||
- .env.appStore.example
|
- 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:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
|
- name: Cache API v1 production build
|
||||||
|
uses: buildjet/cache@v4
|
||||||
|
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: |
|
- run: |
|
||||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
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
|
shell: bash
|
||||||
@@ -30,13 +30,31 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- name: Cache API v2 production build
|
||||||
|
uses: buildjet/cache@v4
|
||||||
|
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: |
|
- run: |
|
||||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
yarn workspace @calcom/api-v2 run generate-schemas
|
if [ ${{ steps.cache-api-v2-build.outputs.cache-hit }} == 'true' ]; then
|
||||||
rm -rf apps/api/v2/node_modules
|
echo "Cache hit for API v2 build. Skipping build."
|
||||||
yarn install
|
else
|
||||||
yarn workspace @calcom/api-v2 run build
|
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
|
shell: bash
|
||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jobs:
|
|||||||
check-types:
|
check-types:
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- name: Show info
|
- name: Show info
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
LAST_DAY=$(date -d tomorrow +%d)
|
LAST_DAY=$(date -d tomorrow +%d)
|
||||||
if [ "$LAST_DAY" == "01" ]; then
|
if [ "$LAST_DAY" == "01" ]; then
|
||||||
echo "::set-output name=is_last_day::true"
|
echo "is_last_day=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "::set-output name=is_last_day::false"
|
echo "is_last_day=false" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: cURL request
|
- name: cURL request
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- uses: buildjet/cache-delete@v1
|
- uses: buildjet/cache-delete@v1
|
||||||
with:
|
with:
|
||||||
cache_key: ${{ inputs.cache_key }}
|
cache_key: ${{ inputs.cache_key }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: E2E App-Store Apps Tests
|
name: E2E App Store Tests
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
@@ -41,7 +41,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-app-store:
|
e2e-app-store:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E App Store (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E App Store
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -70,16 +70,13 @@ jobs:
|
|||||||
- 1025:1025
|
- 1025:1025
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
## There aren't many tests for AppStore. So, just start with 2 shards. Increase if needed.
|
|
||||||
shard: [1, 2]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
@@ -93,10 +90,10 @@ jobs:
|
|||||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
run: yarn e2e:app-store
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: app-store-results
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -41,7 +41,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E Embed React (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E Embed React
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -62,16 +62,13 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
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:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
@@ -79,11 +76,11 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
yarn e2e:embed-react
|
||||||
yarn workspace @calcom/embed-react packaged:tests
|
yarn workspace @calcom/embed-react packaged:tests
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: embed-react-results
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -41,7 +41,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E Embed Core (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E Embed Core
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -70,26 +70,23 @@ jobs:
|
|||||||
- 1025:1025
|
- 1025:1025
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
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:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
run: yarn e2e:embed
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: embed-core-results
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: E2E tests
|
name: E2E
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
@@ -41,7 +41,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E tests (${{ matrix.shard }}/${{ strategy.job-total }})
|
name: E2E (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -71,13 +71,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shard: [1, 2, 3, 4, 5, 6, 7]
|
shard: [1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-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
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v5
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
|
||||||
sync-labels: ""
|
|
||||||
team-labels:
|
team-labels:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: equitybee/team-label-action@main
|
- uses: equitybee/team-label-action@main
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
@@ -39,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Apply labels from linked issue to PR
|
- name: Apply labels from linked issue to PR
|
||||||
uses: actions/github-script@v5
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- name: Run Linting with Reports
|
- name: Run Linting with Reports
|
||||||
|
|||||||
@@ -45,17 +45,11 @@ env:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
|
||||||
name: Production builds
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
|
||||||
secrets: inherit
|
|
||||||
analyze:
|
analyze:
|
||||||
needs: build
|
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: PR Labeled with ready-for-e2e
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [labeled]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- gh-actions-test-branch
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
run-e2e-jobs:
|
||||||
|
name: Run E2E Jobs
|
||||||
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
|
- name: Check Label and Retrigger Checks
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const labelName = context.payload.label.name;
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
|
||||||
|
if (labelName === 'ready-for-e2e') {
|
||||||
|
console.log(`Running E2E jobs for PR #${prNumber}...`);
|
||||||
|
|
||||||
|
const checkRuns = await github.rest.checks.listForRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: context.payload.pull_request.head.sha,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const check of checkRuns.data.check_runs) {
|
||||||
|
console.log('check.name', check.name);
|
||||||
|
if (check.name.includes('e2e')) {
|
||||||
|
await github.rest.actions.reRunJob({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
job_id: check.id,
|
||||||
|
});
|
||||||
|
console.log(`Triggering job #${check.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Triggered E2E checks for PR #${prNumber}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Label ${labelName} does not trigger re-running checks.`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: PR Reviewed
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label-pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Label PR as ready for E2E
|
||||||
|
if: github.event.review.state == 'approved'
|
||||||
|
uses: actions-ecosystem/action-add-labels@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
labels: 'ready-for-e2e'
|
||||||
+95
-34
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
merge_group:
|
- gh-actions-test-branch
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -19,95 +19,156 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
outputs:
|
outputs:
|
||||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||||
|
commit-sha: ${{ steps.get_sha.outputs.commit-sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
has-files-requiring-all-checks:
|
has-files-requiring-all-checks:
|
||||||
- "!(**.md|.github/CODEOWNERS)"
|
- "!(**.md|.github/CODEOWNERS)"
|
||||||
|
- name: Get Latest Commit SHA
|
||||||
|
id: get_sha
|
||||||
|
run: |
|
||||||
|
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
check-label:
|
||||||
|
needs: [changes]
|
||||||
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
name: Check for E2E label
|
||||||
|
outputs:
|
||||||
|
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || (github.event.action == 'labeled' && github.event.label.name == 'ready-for-e2e')) }}
|
||||||
|
steps:
|
||||||
|
- name: Check if PR exists with ready-for-e2e label for this SHA
|
||||||
|
id: check-if-pr-has-label
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let labels = [];
|
||||||
|
|
||||||
|
if (context.payload.pull_request) {
|
||||||
|
labels = context.payload.pull_request.labels;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const sha = '${{ needs.changes.outputs.commit-sha }}';
|
||||||
|
console.log('sha', sha);
|
||||||
|
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
commit_sha: sha
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prs.length === 0) {
|
||||||
|
core.setOutput('run-e2e', false);
|
||||||
|
console.log(`No pull requests found for commit SHA ${sha}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = prs[0];
|
||||||
|
console.log(`PR number: ${pr.number}`);
|
||||||
|
console.log(`PR title: ${pr.title}`);
|
||||||
|
console.log(`PR state: ${pr.state}`);
|
||||||
|
console.log(`PR URL: ${pr.html_url}`);
|
||||||
|
|
||||||
|
labels = pr.labels;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
core.setOutput('run-e2e', false);
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelFound = labels.map(l => l.name).includes('ready-for-e2e');
|
||||||
|
console.log('Found the label?', labelFound);
|
||||||
|
core.setOutput('run-e2e', labelFound);
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
name: Type check
|
name: Type check
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/check-types.yml
|
uses: ./.github/workflows/check-types.yml
|
||||||
secrets: inherit
|
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:
|
lint:
|
||||||
name: Linters
|
name: Linters
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
unit-test:
|
||||||
|
name: Tests
|
||||||
|
needs: [changes, check-label]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/unit-tests.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
integration-test:
|
||||||
|
name: Tests
|
||||||
|
needs: [changes, check-label]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/integration-tests.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
build-api-v1:
|
build-api-v1:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/api-v1-production-build.yml
|
uses: ./.github/workflows/api-v1-production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build-api-v2:
|
build-api-v2:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/api-v2-production-build.yml
|
uses: ./.github/workflows/api-v2-production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
needs: [changes]
|
needs: [changes, check-label]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: E2E tests
|
name: e2e
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
needs: [changes, check-label, build]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e.yml
|
uses: ./.github/workflows/e2e.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-app-store:
|
e2e-app-store:
|
||||||
name: E2E App Store tests
|
name: e2e-app-store
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
needs: [changes, check-label, build]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e-app-store.yml
|
uses: ./.github/workflows/e2e-app-store.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
name: E2E embeds tests
|
name: e2e-embed
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
needs: [changes, check-label, build]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e-embed.yml
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-embed-react:
|
e2e-embed-react:
|
||||||
name: E2E React embeds tests
|
name: e2e-embed-react
|
||||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
needs: [changes, check-label, build]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze Build
|
name: Analyze Build
|
||||||
needs: [changes, build]
|
needs: [build]
|
||||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
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, check-label, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
if: always()
|
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ jobs:
|
|||||||
embed: ${{ steps.filter.outputs.embed }}
|
embed: ${{ steps.filter.outputs.embed }}
|
||||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: "Determine tag"
|
- name: "Determine tag"
|
||||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: main # Always checkout main even for tagged releases
|
ref: main # Always checkout main even for tagged releases
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
@@ -37,12 +37,3 @@ jobs:
|
|||||||
```
|
```
|
||||||
${{ steps.lint_pr_title.outputs.error_message }}
|
${{ 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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Unit tests
|
name: Unit
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
@@ -6,10 +6,11 @@ on:
|
|||||||
types: [completed]
|
types: [completed]
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
name: Unit
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- 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
|
# 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
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
+1
-1
@@ -99,7 +99,7 @@ To develop locally:
|
|||||||
|
|
||||||
- Duplicate `.env.example` to `.env`.
|
- 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 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
|
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:
|
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,15 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
export function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === "edge") {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiResponse, NextApiRequest } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
|
||||||
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
|
|
||||||
|
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
|
||||||
|
checkRateLimitAndThrowError: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("rateLimitApiKey middleware", () => {
|
||||||
|
it("should return 401 if no apiKey is provided", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(res._getStatusCode()).toBe(401);
|
||||||
|
expect(res._getJSONData()).toEqual({ message: "No apiKey provided" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call checkRateLimitAndThrowError with correct parameters", async () => {
|
||||||
|
const { req, res } = createMocks({
|
||||||
|
method: "GET",
|
||||||
|
query: { apiKey: "test-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
|
||||||
|
limit: 100,
|
||||||
|
remaining: 99,
|
||||||
|
reset: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error weird typing between middleware and createMocks
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
||||||
|
identifier: "test-key",
|
||||||
|
rateLimitingType: "api",
|
||||||
|
onRateLimiterResponse: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set rate limit headers correctly", async () => {
|
||||||
|
const { req, res } = createMocks({
|
||||||
|
method: "GET",
|
||||||
|
query: { apiKey: "test-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const rateLimiterResponse = {
|
||||||
|
limit: 100,
|
||||||
|
remaining: 99,
|
||||||
|
reset: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||||
|
onRateLimiterResponse(rateLimiterResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error weird typing between middleware and createMocks
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
||||||
|
expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining);
|
||||||
|
expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 429 if rate limit is exceeded", async () => {
|
||||||
|
const { req, res } = createMocks({
|
||||||
|
method: "GET",
|
||||||
|
query: { apiKey: "test-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
|
||||||
|
|
||||||
|
// @ts-expect-error weird typing between middleware and createMocks
|
||||||
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||||
|
|
||||||
|
expect(res._getStatusCode()).toBe(429);
|
||||||
|
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,15 +6,19 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
|||||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||||
|
|
||||||
// TODO: Add a way to add trusted api keys
|
// TODO: Add a way to add trusted api keys
|
||||||
await checkRateLimitAndThrowError({
|
try {
|
||||||
identifier: req.query.apiKey as string,
|
await checkRateLimitAndThrowError({
|
||||||
rateLimitingType: "api",
|
identifier: req.query.apiKey as string,
|
||||||
onRateLimiterResponse: (response) => {
|
rateLimitingType: "api",
|
||||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
onRateLimiterResponse: (response) => {
|
||||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||||
},
|
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(429).json({ message: "Rate limit exceeded" });
|
||||||
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
@@ -6,6 +6,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { isAdminGuard } from "../utils/isAdmin";
|
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.
|
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
||||||
export const dateNotInPast = function (date: Date) {
|
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" });
|
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
|
// save the user id in the request for later use
|
||||||
req.userId = apiKey.userId;
|
req.userId = apiKey.userId;
|
||||||
// save the isAdmin boolean here for later use
|
const { isAdmin, scope } = await isAdminGuard(req);
|
||||||
req.isAdmin = await isAdminGuard(req);
|
|
||||||
|
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
|
||||||
|
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
@@ -3,9 +3,9 @@ import type { NextMiddleware } from "next-api-middleware";
|
|||||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||||
|
|
||||||
export const verifyCredentialSyncEnabled: NextMiddleware = async (req, res, next) => {
|
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" });
|
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";
|
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 */
|
/** Guard: Only admins can query other users */
|
||||||
if (!isAdmin) {
|
if (!isSystemWideAdmin) {
|
||||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||||
}
|
}
|
||||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||||
|
|||||||
@@ -1,10 +1,37 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
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) => {
|
export const isAdminGuard = async (req: NextApiRequest) => {
|
||||||
const { userId } = req;
|
const { userId } = req;
|
||||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
|
||||||
return user?.role === UserPermissionRole.ADMIN;
|
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,
|
timeZone: true,
|
||||||
locale: true,
|
locale: true,
|
||||||
})
|
})
|
||||||
.optional(),
|
.nullish(),
|
||||||
payment: z
|
payment: z
|
||||||
.array(
|
.array(
|
||||||
_PaymentModel.pick({
|
_PaymentModel.pick({
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ export const withValidQueryIdTransformParseInt = withValidation({
|
|||||||
type: "Zod",
|
type: "Zod",
|
||||||
mode: "query",
|
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 { withSentryConfig } = require("@sentry/nextjs");
|
||||||
|
|
||||||
const plugins = [withAxiom];
|
const plugins = [withAxiom];
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
instrumentationHook: true,
|
||||||
|
},
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@calcom/app-store",
|
"@calcom/app-store",
|
||||||
"@calcom/core",
|
"@calcom/core",
|
||||||
@@ -87,12 +92,12 @@ const nextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
nextConfig["sentry"] = {
|
plugins.push((nextConfig) =>
|
||||||
autoInstrumentServerFunctions: true,
|
withSentryConfig(nextConfig, {
|
||||||
hideSourceMaps: true,
|
autoInstrumentServerFunctions: true,
|
||||||
};
|
hideSourceMaps: true,
|
||||||
|
})
|
||||||
plugins.push(withSentryConfig);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
Vendored
+2
-1
@@ -11,7 +11,8 @@ export declare module "next" {
|
|||||||
method: string;
|
method: string;
|
||||||
// session: { user: { id: number } };
|
// session: { user: { id: number } };
|
||||||
// query: Partial<{ [key: string]: string | string[] }>;
|
// query: Partial<{ [key: string]: string | string[] }>;
|
||||||
isAdmin: boolean;
|
isSystemWideAdmin: boolean;
|
||||||
|
isOrganizationOwnerOrAdmin: boolean;
|
||||||
pagination: { take: number; skip: number };
|
pagination: { take: number; skip: number };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@sentry/nextjs": "^7.73.0",
|
"@sentry/nextjs": "^8.8.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.5.4",
|
"next": "^13.5.4",
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
|||||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||||
|
|
||||||
export async function authMiddleware(req: NextApiRequest) {
|
export async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||||
// Admin can check any api key
|
// Admin can check any api key
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
// Check if user can access the api key
|
// Check if user can access the api key
|
||||||
const apiKey = await prisma.apiKey.findFirst({
|
const apiKey = await prisma.apiKey.findFirst({
|
||||||
where: { id, userId },
|
where: { id, userId },
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ type CustomNextApiRequest = NextApiRequest & {
|
|||||||
function handleAdminRequests(req: CustomNextApiRequest) {
|
function handleAdminRequests(req: CustomNextApiRequest) {
|
||||||
// To match type safety with runtime
|
// To match type safety with runtime
|
||||||
if (!hasReqArgs(req)) throw Error("Missing req.args");
|
if (!hasReqArgs(req)) throw Error("Missing req.args");
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
if (isAdmin && req.query.userId) {
|
if (isSystemWideAdmin && req.query.userId) {
|
||||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||||
req.args.where = { userId: { in: userIds } };
|
req.args.where = { userId: { in: userIds } };
|
||||||
@@ -30,8 +30,8 @@ function hasReqArgs(req: CustomNextApiRequest): req is Ensure<CustomNextApiReque
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getHandler(req: CustomNextApiRequest) {
|
async function getHandler(req: CustomNextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
req.args = isAdmin ? {} : { where: { userId } };
|
req.args = isSystemWideAdmin ? {} : { where: { userId } };
|
||||||
// Proof of concept: allowing mutation in exchange of composability
|
// Proof of concept: allowing mutation in exchange of composability
|
||||||
handleAdminRequests(req);
|
handleAdminRequests(req);
|
||||||
const data = await prisma.apiKey.findMany(req.args);
|
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";
|
import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
||||||
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
||||||
const args: Prisma.ApiKeyCreateArgs = {
|
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 };
|
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||||
await prisma.user.findFirstOrThrow({ where });
|
await prisma.user.findFirstOrThrow({ where });
|
||||||
args.data.userId = bodyUserId;
|
args.data.userId = bodyUserId;
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
|||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const query = schemaQueryIdParseInt.parse(req.query);
|
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.
|
// @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
|
// Find all user bookings, including attendees
|
||||||
const attendee = await prisma.attendee.findFirst({
|
const attendee = await prisma.attendee.findFirst({
|
||||||
where: { id: query.id, booking: { userId } },
|
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>) {
|
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaAttendeeEditBodyParams>) {
|
||||||
const { isAdmin } = req;
|
const { isSystemWideAdmin } = req;
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
const { userId } = req;
|
const { userId } = req;
|
||||||
const { bookingId } = body;
|
const { bookingId } = body;
|
||||||
if (bookingId) {
|
if (bookingId) {
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ import { schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
|||||||
* description: No attendees were found
|
* description: No attendees were found
|
||||||
*/
|
*/
|
||||||
async function handler(req: NextApiRequest) {
|
async function handler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const args: Prisma.AttendeeFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } };
|
const args: Prisma.AttendeeFindManyArgs = isSystemWideAdmin ? {} : { where: { booking: { userId } } };
|
||||||
const data = await prisma.attendee.findMany(args);
|
const data = await prisma.attendee.findMany(args);
|
||||||
const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee));
|
const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee));
|
||||||
if (!attendees) throw new HttpError({ statusCode: 404, message: "No attendees were found" });
|
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.
|
* description: Authorization information is missing or invalid.
|
||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const body = schemaAttendeeCreateBodyParams.parse(req.body);
|
const body = schemaAttendeeCreateBodyParams.parse(req.body);
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isSystemWideAdmin) {
|
||||||
const userBooking = await prisma.booking.findFirst({
|
const userBooking = await prisma.booking.findFirst({
|
||||||
where: { userId, id: body.bookingId },
|
where: { userId, id: body.bookingId },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import prisma from "@calcom/prisma";
|
|||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, query } = req;
|
const { userId, isSystemWideAdmin, query } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
/** Admins can skip the ownership verification */
|
/** 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,
|
* 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.
|
* 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) {
|
async function checkPermissions(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
||||||
const schedule = await prisma.schedule.findFirst({
|
const schedule = await prisma.schedule.findFirst({
|
||||||
where: { userId, id: data.scheduleId },
|
where: { userId, id: data.scheduleId },
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const availabilitySchema = z
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest) {
|
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);
|
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||||
if (!teamId)
|
if (!teamId)
|
||||||
return getUserAvailability({
|
return getUserAvailability({
|
||||||
@@ -224,7 +224,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
const isUserAdminOrOwner =
|
const isUserAdminOrOwner =
|
||||||
memberRoles[reqUserId] == MembershipRole.ADMIN ||
|
memberRoles[reqUserId] == MembershipRole.ADMIN ||
|
||||||
memberRoles[reqUserId] == MembershipRole.OWNER ||
|
memberRoles[reqUserId] == MembershipRole.OWNER ||
|
||||||
isAdmin;
|
isSystemWideAdmin;
|
||||||
if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
const availabilities = members.map(async (user) => {
|
const availabilities = members.map(async (user) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
|||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
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.
|
// 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
|
// Find all references where the user has bookings
|
||||||
const bookingReference = await prisma.bookingReference.findFirst({
|
const bookingReference = await prisma.bookingReference.findFirst({
|
||||||
where: { id, booking: { userId } },
|
where: { id, booking: { userId } },
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* description: Authorization information is missing or invalid.
|
* description: Authorization information is missing or invalid.
|
||||||
*/
|
*/
|
||||||
export async function patchHandler(req: NextApiRequest) {
|
export async function patchHandler(req: NextApiRequest) {
|
||||||
const { query, body, isAdmin, userId } = req;
|
const { query, body, isSystemWideAdmin, userId } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const data = schemaBookingEditBodyParams.parse(body);
|
const data = schemaBookingEditBodyParams.parse(body);
|
||||||
/* If user tries to update bookingId, we run extra checks */
|
/* If user tries to update bookingId, we run extra checks */
|
||||||
if (data.bookingId) {
|
if (data.bookingId) {
|
||||||
const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin
|
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||||
? /* If admin, we only check that the booking exists */
|
? /* If admin, we only check that the booking exists */
|
||||||
{ where: { id: data.bookingId } }
|
{ where: { id: data.bookingId } }
|
||||||
: /* For non-admins we make sure the booking belongs to the user */
|
: /* 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
|
* description: No booking references were found
|
||||||
*/
|
*/
|
||||||
async function getHandler(req: NextApiRequest) {
|
async function getHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const args: Prisma.BookingReferenceFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } };
|
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||||
|
? {}
|
||||||
|
: { where: { booking: { userId } } };
|
||||||
const data = await prisma.bookingReference.findMany(args);
|
const data = await prisma.bookingReference.findMany(args);
|
||||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ import {
|
|||||||
* description: Authorization information is missing or invalid.
|
* description: Authorization information is missing or invalid.
|
||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const body = schemaBookingCreateBodyParams.parse(req.body);
|
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 */
|
? /* If admin, we only check that the booking exists */
|
||||||
{ where: { id: body.bookingId } }
|
{ where: { id: body.bookingId } }
|
||||||
: /* For non-admins we make sure the booking belongs to the user */
|
: /* 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 { HttpError } from "@calcom/lib/http-error";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, query } = req;
|
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin, query } = req;
|
||||||
if (isAdmin) {
|
if (isSystemWideAdmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
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({
|
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
include: {
|
include: {
|
||||||
@@ -43,7 +61,7 @@ async function authMiddleware(req: NextApiRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!teamBookings) {
|
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 { defaultResponder } from "@calcom/lib/server";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
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>) {
|
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
|
||||||
const { isAdmin } = req;
|
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||||
if (body.userId && !isAdmin) {
|
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
|
// 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({
|
throw new HttpError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: "Only admin can change the organizer of a booking",
|
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);
|
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 { defaultResponder } from "@calcom/lib/server";
|
||||||
import prisma from "@calcom/prisma";
|
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 { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||||
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
|
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
|
||||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||||
@@ -108,7 +113,11 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
|||||||
* 404:
|
* 404:
|
||||||
* description: No bookings were found
|
* description: No bookings were found
|
||||||
*/
|
*/
|
||||||
|
type GetAdminArgsType = {
|
||||||
|
adminDidQueryUserIds?: boolean;
|
||||||
|
requestedUserIds: number[];
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Constructs the WHERE clause for Prisma booking findMany operation.
|
* Constructs the WHERE clause for Prisma booking findMany operation.
|
||||||
*
|
*
|
||||||
@@ -161,12 +170,20 @@ function buildWhereClause(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(req: NextApiRequest) {
|
export async function handler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const {
|
||||||
|
userId,
|
||||||
|
isSystemWideAdmin,
|
||||||
|
isOrganizationOwnerOrAdmin,
|
||||||
|
pagination: { take, skip },
|
||||||
|
} = req;
|
||||||
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
|
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
|
||||||
|
|
||||||
const args: Prisma.BookingFindManyArgs = {};
|
const args: Prisma.BookingFindManyArgs = {};
|
||||||
|
if (req.query.take && req.query.page) {
|
||||||
|
args.take = take;
|
||||||
|
args.skip = skip;
|
||||||
|
}
|
||||||
args.include = {
|
args.include = {
|
||||||
attendees: true,
|
attendees: true,
|
||||||
user: true,
|
user: true,
|
||||||
@@ -182,19 +199,32 @@ async function handler(req: NextApiRequest) {
|
|||||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||||
|
|
||||||
/** Only admins can query other users */
|
/** Only admins can query other users */
|
||||||
if (isAdmin) {
|
if (isSystemWideAdmin) {
|
||||||
if (req.query.userId) {
|
if (req.query.userId || filterByAttendeeEmails) {
|
||||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
const requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||||
const users = await prisma.user.findMany({
|
|
||||||
where: { id: { in: userIds } },
|
const systemWideAdminArgs = {
|
||||||
select: { email: true },
|
adminDidQueryUserIds: !!req.query.userId,
|
||||||
});
|
requestedUserIds,
|
||||||
const userEmails = users.map((u) => u.email);
|
userId,
|
||||||
args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails);
|
};
|
||||||
} else if (filterByAttendeeEmails) {
|
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
||||||
args.where = buildWhereClause(null, attendeeEmails, [], []);
|
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 {
|
} else {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
@@ -203,7 +233,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!user) {
|
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, [], []);
|
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||||
}
|
}
|
||||||
@@ -225,4 +255,53 @@ async function handler(req: NextApiRequest) {
|
|||||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
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 getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
||||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
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 { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /bookings:
|
* /bookings:
|
||||||
@@ -204,10 +208,28 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
* description: Authorization information is missing or invalid.
|
* description: Authorization information is missing or invalid.
|
||||||
*/
|
*/
|
||||||
async function handler(req: NextApiRequest) {
|
async function handler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||||
if (isAdmin) req.userId = req.body.userId || userId;
|
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);
|
export default defaultResponder(handler);
|
||||||
@@ -97,9 +97,10 @@ import { schemaConnectedCalendarsReadPublic } from "~/lib/validations/connected-
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async function getHandler(req: NextApiRequest) {
|
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];
|
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
|||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||||
// Admins can just skip this check
|
// 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
|
// Check if the current user can access the event type of this input
|
||||||
const eventTypeCustomInput = await prisma.eventTypeCustomInput.findFirst({
|
const eventTypeCustomInput = await prisma.eventTypeCustomInput.findFirst({
|
||||||
where: { id, eventType: { userId } },
|
where: { id, eventType: { userId } },
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-c
|
|||||||
* description: No eventTypeCustomInputs were found
|
* description: No eventTypeCustomInputs were found
|
||||||
*/
|
*/
|
||||||
async function getHandler(req: NextApiRequest) {
|
async function getHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const args: Prisma.EventTypeCustomInputFindManyArgs = isAdmin ? {} : { where: { eventType: { userId } } };
|
const args: Prisma.EventTypeCustomInputFindManyArgs = isSystemWideAdmin
|
||||||
|
? {}
|
||||||
|
: { where: { eventType: { userId } } };
|
||||||
const data = await prisma.eventTypeCustomInput.findMany(args);
|
const data = await prisma.eventTypeCustomInput.findMany(args);
|
||||||
return { event_type_custom_inputs: data.map((v) => schemaEventTypeCustomInputPublic.parse(v)) };
|
return { event_type_custom_inputs: data.map((v) => schemaEventTypeCustomInputPublic.parse(v)) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ import {
|
|||||||
* description: Authorization information is missing or invalid.
|
* description: Authorization information is missing or invalid.
|
||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { eventTypeId, ...body } = schemaEventTypeCustomInputBodyParams.parse(req.body);
|
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. */
|
/* 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({
|
const eventType = await prisma.eventType.findFirst({
|
||||||
where: { id: eventTypeId, userId },
|
where: { id: eventTypeId, userId },
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import prisma from "@calcom/prisma";
|
|||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
const userEventTypes = await prisma.eventType.findMany({
|
const userEventTypes = await prisma.eventType.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ type UserCredentialType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function patchHandler(req: NextApiRequest) {
|
export async function patchHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, query, body } = req;
|
const { userId, isSystemWideAdmin, query, body } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
const assignedUserId = isSystemWideAdmin ? parsedBody.userId || userId : userId;
|
||||||
|
|
||||||
validateIntegrationInput(parsedBody);
|
validateIntegrationInput(parsedBody);
|
||||||
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ import {
|
|||||||
* description: Authorization information is missing or invalid.
|
* description: Authorization information is missing or invalid.
|
||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, body } = req;
|
const { userId, isSystemWideAdmin, body } = req;
|
||||||
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||||
await checkPermissions(req, userId);
|
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 */
|
/* Check if credentialId data matches the ownership and integration passed in */
|
||||||
const userCredentials = await prisma.credential.findMany({
|
const userCredentials = await prisma.credential.findMany({
|
||||||
@@ -120,19 +120,20 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest, userId: number) {
|
async function checkPermissions(req: NextApiRequest, userId: number) {
|
||||||
const { isAdmin } = req;
|
const { isSystemWideAdmin } = req;
|
||||||
const body = schemaDestinationCalendarCreateBodyParams.parse(req.body);
|
const body = schemaDestinationCalendarCreateBodyParams.parse(req.body);
|
||||||
|
|
||||||
/* Non-admin users can only create destination calendars for themselves */
|
/* Non-admin users can only create destination calendars for themselves */
|
||||||
if (!isAdmin && body.userId)
|
if (!isSystemWideAdmin && body.userId)
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: "ADMIN required for `userId`",
|
message: "ADMIN required for `userId`",
|
||||||
});
|
});
|
||||||
/* Admin users are required to pass in a 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*/
|
/* 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 } });
|
const ownsEventType = await prisma.eventType.findFirst({ where: { id: body.eventTypeId, userId } });
|
||||||
if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,37 @@ const swaggerHandler = withSwagger({
|
|||||||
$ref: "#/components/schemas/Booking",
|
$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: {
|
Booking: {
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ export async function deleteHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest) {
|
async function checkPermissions(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
/** Only event type owners can delete it */
|
/** Only event type owners can delete it */
|
||||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
|||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
||||||
) {
|
) {
|
||||||
if (req.isAdmin) return true;
|
if (req.isSystemWideAdmin) return true;
|
||||||
if (eventType?.teamId) {
|
if (eventType?.teamId) {
|
||||||
req.query.teamId = String(eventType.teamId);
|
req.query.teamId = String(eventType.teamId);
|
||||||
await canAccessTeamEventOrThrow(req, {
|
await canAccessTeamEventOrThrow(req, {
|
||||||
|
|||||||
@@ -237,9 +237,9 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
/** Only event type owners can modify it */
|
/** Only event type owners can modify it */
|
||||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ import getCalLink from "./_utils/getCalLink";
|
|||||||
* description: No event types were found
|
* description: No event types were found
|
||||||
*/
|
*/
|
||||||
async function getHandler(req: NextApiRequest) {
|
async function getHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||||
const { slug } = schemaQuerySlug.parse(req.query);
|
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.
|
// 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.
|
// But currently we return only the event types of the user. Not changing this for backwards compatibility.
|
||||||
const data = await prisma.eventType.findMany({
|
const data = await prisma.eventType.findMany({
|
||||||
@@ -74,9 +74,9 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// TODO: Extract & reuse.
|
// TODO: Extract & reuse.
|
||||||
function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||||
/** Guard: Only admins can query other users */
|
/** Guard: Only admins can query other users */
|
||||||
if (!isAdmin) {
|
if (!isSystemWideAdmin) {
|
||||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||||
}
|
}
|
||||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
|||||||
* description: Authorization information is missing or invalid.
|
* description: Authorization information is missing or invalid.
|
||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, body } = req;
|
const { userId, isSystemWideAdmin, body } = req;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hosts = [],
|
hosts = [],
|
||||||
@@ -291,7 +291,7 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
await checkUserMembership(req);
|
await checkUserMembership(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdmin && parsedBody.userId) {
|
if (isSystemWideAdmin && parsedBody.userId) {
|
||||||
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,18 +311,18 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest) {
|
async function checkPermissions(req: NextApiRequest) {
|
||||||
const { isAdmin } = req;
|
const { isSystemWideAdmin } = req;
|
||||||
const body = schemaEventTypeCreateBodyParams.parse(req.body);
|
const body = schemaEventTypeCreateBodyParams.parse(req.body);
|
||||||
/* Non-admin users can only create event types for themselves */
|
/* Non-admin users can only create event types for themselves */
|
||||||
if (!isAdmin && body.userId)
|
if (!isSystemWideAdmin && body.userId)
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: "ADMIN required for `userId`",
|
message: "ADMIN required for `userId`",
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
body.teamId &&
|
body.teamId &&
|
||||||
!isAdmin &&
|
!isSystemWideAdmin &&
|
||||||
!(await canUserAccessTeamWithRole(req.userId, isAdmin, body.teamId, {
|
!(await canUserAccessTeamWithRole(req.userId, isSystemWideAdmin, body.teamId, {
|
||||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
@@ -331,7 +331,7 @@ async function checkPermissions(req: NextApiRequest) {
|
|||||||
message: "ADMIN required for `teamId`",
|
message: "ADMIN required for `teamId`",
|
||||||
});
|
});
|
||||||
/* Admin users are required to pass in a userId or 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" });
|
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export default async function checkTeamEventEditPermission(
|
|||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
|
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
|
||||||
) {
|
) {
|
||||||
const { isAdmin } = req;
|
const { isSystemWideAdmin } = req;
|
||||||
let userId = req.userId;
|
let userId = req.userId;
|
||||||
if (isAdmin && body.userId) {
|
if (isSystemWideAdmin && body.userId) {
|
||||||
userId = body.userId;
|
userId = body.userId;
|
||||||
}
|
}
|
||||||
if (body.teamId) {
|
if (body.teamId) {
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) {
|
async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
// 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`" });
|
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||||
// Only team OWNERS and ADMINS can add other members
|
// Only team OWNERS and ADMINS can add other members
|
||||||
const membership = await prisma.membership.findFirst({
|
const membership = await prisma.membership.findFirst({
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user";
|
|||||||
|
|
||||||
async function handler({ userId }: NextApiRequest) {
|
async function handler({ userId }: NextApiRequest) {
|
||||||
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
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);
|
export default defaultResponder(handler);
|
||||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
|||||||
import { membershipIdSchema } from "~/lib/validations/membership";
|
import { membershipIdSchema } from "~/lib/validations/membership";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { teamId } = membershipIdSchema.parse(req.query);
|
const { teamId } = membershipIdSchema.parse(req.query);
|
||||||
// Admins can just skip this check
|
// Admins can just skip this check
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
// Only team members can modify a membership
|
// Only team members can modify a membership
|
||||||
const membership = await prisma.membership.findFirst({ where: { userId, teamId } });
|
const membership = await prisma.membership.findFirst({ where: { userId, teamId } });
|
||||||
if (!membership) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!membership) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ export async function deleteHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest) {
|
async function checkPermissions(req: NextApiRequest) {
|
||||||
const { isAdmin, userId, query } = req;
|
const { isSystemWideAdmin, userId, query } = req;
|
||||||
const userId_teamId = membershipIdSchema.parse(query);
|
const userId_teamId = membershipIdSchema.parse(query);
|
||||||
// Admin User can do anything including deletion of Admin Team Member in any team
|
// Admin User can do anything including deletion of Admin Team Member in any team
|
||||||
if (isAdmin) {
|
if (isSystemWideAdmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest) {
|
async function checkPermissions(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { userId: queryUserId, teamId } = membershipIdSchema.parse(req.query);
|
const { userId: queryUserId, teamId } = membershipIdSchema.parse(req.query);
|
||||||
const data = membershipEditBodySchema.parse(req.body);
|
const data = membershipEditBodySchema.parse(req.body);
|
||||||
// Admins can just skip this check
|
// Admins can just skip this check
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
// Only the invited user can accept the invite
|
// Only the invited user can accept the invite
|
||||||
if ("accepted" in data && queryUserId !== userId)
|
if ("accepted" in data && queryUserId !== userId)
|
||||||
throw new HttpError({
|
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
|
* Returns requested users IDs only if admin, otherwise return only current user ID
|
||||||
*/
|
*/
|
||||||
function getUserIds(req: NextApiRequest) {
|
function getUserIds(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
/** Only admins can query other users */
|
/** Only admins can query other users */
|
||||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
if (!isSystemWideAdmin && req.query.userId)
|
||||||
if (isAdmin && req.query.userId) {
|
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||||
|
if (isSystemWideAdmin && req.query.userId) {
|
||||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||||
return userIds;
|
return userIds;
|
||||||
}
|
}
|
||||||
// Return all memberships for ADMIN, limit to current user to non-admins
|
// 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
|
* Returns requested teams IDs only if admin
|
||||||
*/
|
*/
|
||||||
function getTeamIds(req: NextApiRequest) {
|
function getTeamIds(req: NextApiRequest) {
|
||||||
const { isAdmin } = req;
|
const { isSystemWideAdmin } = req;
|
||||||
/** Only admins can query other teams */
|
/** Only admins can query other teams */
|
||||||
if (!isAdmin && req.query.teamId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
if (!isSystemWideAdmin && req.query.teamId)
|
||||||
if (isAdmin && req.query.teamId) {
|
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||||
|
if (isSystemWideAdmin && req.query.teamId) {
|
||||||
const query = schemaQuerySingleOrMultipleTeamIds.parse(req.query);
|
const query = schemaQuerySingleOrMultipleTeamIds.parse(req.query);
|
||||||
const teamIds = Array.isArray(query.teamId) ? query.teamId : [query.teamId];
|
const teamIds = Array.isArray(query.teamId) ? query.teamId : [query.teamId];
|
||||||
return teamIds;
|
return teamIds;
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest) {
|
async function checkPermissions(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
const body = membershipCreateBodySchema.parse(req.body);
|
const body = membershipCreateBodySchema.parse(req.body);
|
||||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
// 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`" });
|
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||||
// Only team OWNERS and ADMINS can add other members
|
// Only team OWNERS and ADMINS can add other members
|
||||||
const membership = await prisma.membership.findFirst({
|
const membership = await prisma.membership.findFirst({
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
|||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||||
// Admins can just skip this check
|
// Admins can just skip this check
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
// Check if the current user can access the schedule
|
// Check if the current user can access the schedule
|
||||||
const schedule = await prisma.schedule.findFirst({
|
const schedule = await prisma.schedule.findFirst({
|
||||||
where: { id, userId },
|
where: { id, userId },
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaSingleScheduleBodyParams>) {
|
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaSingleScheduleBodyParams>) {
|
||||||
const { isAdmin } = req;
|
const { isSystemWideAdmin } = req;
|
||||||
if (isAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
if (body.userId) {
|
if (body.userId) {
|
||||||
throw new HttpError({ statusCode: 403, message: "Non admin cannot change the owner of a schedule" });
|
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) {
|
async function handler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const args: Prisma.ScheduleFindManyArgs = isAdmin ? {} : { where: { userId } };
|
const args: Prisma.ScheduleFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||||
args.include = { availability: true };
|
args.include = { availability: true };
|
||||||
|
|
||||||
if (!isAdmin && req.query.userId)
|
if (!isSystemWideAdmin && req.query.userId)
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: "Unauthorized: Only admins can query other users",
|
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 query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||||
args.where = { userId: { in: userIds } };
|
args.where = { userId: { in: userIds } };
|
||||||
|
|||||||
@@ -80,14 +80,14 @@ import { schemaCreateScheduleBodyParams, schemaSchedulePublic } from "~/lib/vali
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const body = schemaCreateScheduleBodyParams.parse(req.body);
|
const body = schemaCreateScheduleBodyParams.parse(req.body);
|
||||||
let args: Prisma.ScheduleCreateArgs = { data: { ...body, userId } };
|
let args: Prisma.ScheduleCreateArgs = { data: { ...body, userId } };
|
||||||
|
|
||||||
/* If ADMIN we create the schedule for selected user */
|
/* 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`" });
|
throw new HttpError({ statusCode: 403, message: "ADMIN required for `userId`" });
|
||||||
|
|
||||||
// We create default availabilities for the schedule
|
// We create default availabilities for the schedule
|
||||||
|
|||||||
Loaded 100 of 968 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user