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 | ||
|
|
46f67bba9b | ||
|
|
d9806da049 | ||
|
|
42fc49488d | ||
|
|
e030fe0e07 | ||
|
|
51428087ef | ||
|
|
3942df369d | ||
|
|
1d309a6f09 | ||
|
|
56f31eee7a | ||
|
|
2dae0bfb8c | ||
|
|
917c7b0764 | ||
|
|
fd64b0f468 | ||
|
|
b1909337db | ||
|
|
039bf6d4e3 | ||
|
|
83e15c4ce1 | ||
|
|
7d6ecf7201 | ||
|
|
bc42c27230 | ||
|
|
57abb26c31 | ||
|
|
a136511c8a | ||
|
|
c65dca9486 | ||
|
|
816d31918d | ||
|
|
097a69d7d7 | ||
|
|
c2a07e221d | ||
|
|
6d8fbfc49a | ||
|
|
304ef29618 | ||
|
|
b8890c841c | ||
|
|
2f5768b74f | ||
|
|
c1416173e8 | ||
|
|
726af4970c | ||
|
|
d15b7d8168 | ||
|
|
c5bd45fdfb | ||
|
|
5d7b2a12a7 | ||
|
|
7c90d1ed76 | ||
|
|
5ca10d1e83 | ||
|
|
f0e59c2331 | ||
|
|
f812d92045 | ||
|
|
23a17c4e8a | ||
|
|
5beb039003 | ||
|
|
63dd357664 | ||
|
|
b6a57381c7 | ||
|
|
7cf840723b | ||
|
|
bc0c40d260 | ||
|
|
89612a7a68 | ||
|
|
a2620a0e50 | ||
|
|
cfcf5b168c | ||
|
|
3f4d9962a7 | ||
|
|
83917e8655 | ||
|
|
f4bd0dbcc9 | ||
|
|
c87b8b43f9 | ||
|
|
b2baaea84c | ||
|
|
0e431ba7c6 | ||
|
|
b62f6eee3c | ||
|
|
2d0cac0dad | ||
|
|
0c3d1b3034 | ||
|
|
74f6d7686b | ||
|
|
49601bed6f | ||
|
|
b9659051d4 | ||
|
|
9c96507af2 | ||
|
|
2352babe93 | ||
|
|
1467815c3f | ||
|
|
7254cd97e2 | ||
|
|
4cb77abfd7 | ||
|
|
8deedfd361 | ||
|
|
3a0c766ed5 | ||
|
|
4f57e3201d | ||
|
|
e404f7eb88 | ||
|
|
5c2b561a5c | ||
|
|
57b404f721 | ||
|
|
df9a6621cf | ||
|
|
c478f73327 | ||
|
|
88f4dd2446 | ||
|
|
97b2816897 | ||
|
|
00372b6b3b | ||
|
|
6c8fced247 | ||
|
|
52813b01e6 | ||
|
|
77a98d96c8 | ||
|
|
676eac357f | ||
|
|
5bafd1b264 | ||
|
|
0391bfe887 | ||
|
|
c18407d75a | ||
|
|
a631286770 | ||
|
|
dd78741158 | ||
|
|
8f69811e25 | ||
|
|
3a63a8b228 | ||
|
|
ef98105503 | ||
|
|
a18f1eb382 | ||
|
|
2ab6315a8e | ||
|
|
258ebe0420 | ||
|
|
1cfd2f7053 | ||
|
|
a51024e915 | ||
|
|
900e90cd2c | ||
|
|
46ffeaee82 | ||
|
|
22425bb693 | ||
|
|
ad0b8a668c | ||
|
|
bbad2c9d31 | ||
|
|
8b28a6f5df | ||
|
|
93bb37702c | ||
|
|
4a8f251d89 | ||
|
|
cf44919cdd | ||
|
|
024e40b26b | ||
|
|
7c354321be | ||
|
|
fc16ec59d3 | ||
|
|
ab26481738 | ||
|
|
2fbc06314e | ||
|
|
386006fad0 | ||
|
|
ceb555460e | ||
|
|
c28870e029 | ||
|
|
0650898302 | ||
|
|
d004815a68 | ||
|
|
5d76bd6b9c | ||
|
|
d02947f887 | ||
|
|
a3a95a1496 | ||
|
|
d47c6b3fdb | ||
|
|
ebbb1dcce3 | ||
|
|
8fbb1ed426 | ||
|
|
d8adc8eeb1 | ||
|
|
a7bd4aee85 | ||
|
|
82fcda4b42 | ||
|
|
b3b7a09cfb |
@@ -299,6 +299,7 @@ E2E_TEST_CALCOM_GCAL_KEYS=
|
|||||||
CALCOM_CREDENTIAL_SYNC_SECRET=""
|
CALCOM_CREDENTIAL_SYNC_SECRET=""
|
||||||
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
|
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
|
||||||
CALCOM_CREDENTIAL_SYNC_HEADER_NAME="calcom-credential-sync-secret"
|
CALCOM_CREDENTIAL_SYNC_HEADER_NAME="calcom-credential-sync-secret"
|
||||||
|
# This the endpoint from which the token is fetched
|
||||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||||
# Key should match on Cal.com and your application
|
# Key should match on Cal.com and your application
|
||||||
# must be 24 bytes for AES256 encryption algorithm
|
# must be 24 bytes for AES256 encryption algorithm
|
||||||
@@ -358,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
|
||||||
|
|||||||
@@ -2,29 +2,18 @@
|
|||||||
|
|
||||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||||
|
|
||||||
Fixes # (issue)
|
- Fixes #XXXX (GitHub issue number)
|
||||||
|
- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)
|
||||||
|
|
||||||
<!-- Please provide a loom video for visual changes to speed up reviews
|
<!-- Please provide a loom video for visual changes to speed up reviews
|
||||||
Loom Video: https://www.loom.com/
|
Loom Video: https://www.loom.com/
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Requirement/Documentation
|
## Mandatory Tasks (DO NOT REMOVE)
|
||||||
|
|
||||||
<!-- Please provide all documents that are important to understand the reason of that PR. -->
|
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected)
|
||||||
|
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com)
|
||||||
- If there is a requirement document, please, share it here.
|
- [ ] I have added or modified automated tests that prove my fix is effective or that my feature works (PRs might be rejected if logical changes are not properly tested)
|
||||||
- If there is a UI/UX design document, please, share it here.
|
|
||||||
|
|
||||||
## Type of change
|
|
||||||
|
|
||||||
<!-- Please delete bullets that are not relevant. -->
|
|
||||||
|
|
||||||
- Bug fix (non-breaking change which fixes an issue)
|
|
||||||
- Chore (refactoring code, technical debt, workflow improvements)
|
|
||||||
- New feature (non-breaking change which adds functionality)
|
|
||||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
|
||||||
- Tests (Unit/Integration/E2E or any other test)
|
|
||||||
- This change requires a documentation update
|
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|
||||||
@@ -35,10 +24,6 @@ Fixes # (issue)
|
|||||||
- What is expected (happy path) to have (input and output)?
|
- What is expected (happy path) to have (input and output)?
|
||||||
- Any other important info that could help to test that PR
|
- Any other important info that could help to test that PR
|
||||||
|
|
||||||
## Mandatory Tasks
|
|
||||||
|
|
||||||
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
|
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!-- Remove bullet points below that don't apply to you -->
|
<!-- Remove bullet points below that don't apply to you -->
|
||||||
@@ -46,7 +31,4 @@ Fixes # (issue)
|
|||||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||||
- My code doesn't follow the style guidelines of this project
|
- My code doesn't follow the style guidelines of this project
|
||||||
- I haven't commented my code, particularly in hard-to-understand areas
|
- I haven't commented my code, particularly in hard-to-understand areas
|
||||||
- I haven't checked if my PR needs changes to the documentation
|
|
||||||
- I haven't checked if my changes generate no new warnings
|
- I haven't checked if my changes generate no new warnings
|
||||||
- I haven't added tests that prove my fix is effective or that my feature works
|
|
||||||
- I haven't checked if new and existing unit tests pass locally with my changes
|
|
||||||
|
|||||||
@@ -8,22 +8,26 @@ 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
|
||||||
|
# and not the branch that's attempting to merge to main, which causes CI errors when merged
|
||||||
|
# to main.
|
||||||
|
# TODO: Fix this problem if intending to modify this cache key.
|
||||||
cache-name: prod-build
|
cache-name: prod-build
|
||||||
key-1: ${{ inputs.node_version }}-${{ hashFiles('yarn.lock') }}
|
key-1: ${{ inputs.node_version }}-${{ hashFiles('yarn.lock') }}
|
||||||
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 }}
|
||||||
# Ensures production-build.yml will always be fresh
|
|
||||||
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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Production Build
|
name: Production Builds
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
@@ -18,7 +18,10 @@ env:
|
|||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
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_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_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||||
@@ -33,13 +36,10 @@ env:
|
|||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
|
||||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
|
||||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build API V1
|
name: Build API v1
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
services:
|
services:
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: Production Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build API v2
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
timeout-minutes: 30
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:13
|
||||||
|
credentials:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: calendso
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- 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: |
|
||||||
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
|
if [ ${{ steps.cache-api-v2-build.outputs.cache-hit }} == 'true' ]; then
|
||||||
|
echo "Cache hit for API v2 build. Skipping build."
|
||||||
|
else
|
||||||
|
yarn workspace @calcom/api-v2 run generate-schemas
|
||||||
|
rm -rf apps/api/v2/node_modules
|
||||||
|
yarn install
|
||||||
|
yarn workspace @calcom/api-v2 run build
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
@@ -6,10 +6,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
runs-on: buildjet-4vcpu-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: |
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
echo "Fetching list of cache key"
|
echo "Fetching list of cache key"
|
||||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||||
|
|
||||||
## Setting this to not fail the workflow while deleting cache keys.
|
## Setting this to not fail the workflow while deleting cache keys.
|
||||||
set +e
|
set +e
|
||||||
echo "Deleting caches..."
|
echo "Deleting caches..."
|
||||||
for cacheKey in $cacheKeysForPR
|
for cacheKey in $cacheKeysForPR
|
||||||
|
|||||||
@@ -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,12 +1,47 @@
|
|||||||
name: E2E App-Store Apps Tests
|
name: E2E App Store Tests
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
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:
|
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:
|
||||||
@@ -35,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
|
||||||
@@ -58,44 +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
|
||||||
env:
|
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
|
||||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
|
||||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
|
||||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
|
||||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
|
||||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
|
||||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
|
||||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
|
||||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
|
||||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
|
||||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
|
||||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
|
||||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
|
||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
|
||||||
- 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
|
||||||
|
|||||||
@@ -3,10 +3,45 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
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:
|
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:
|
||||||
@@ -27,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
|
||||||
@@ -44,40 +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
|
||||||
env:
|
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
|
||||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
|
||||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
|
||||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
|
||||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
|
||||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
|
||||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
|
||||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
|
||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
|
||||||
- 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
|
||||||
|
|||||||
@@ -3,10 +3,45 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
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:
|
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:
|
||||||
@@ -35,56 +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
|
||||||
env:
|
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
|
||||||
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 }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
|
||||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
|
||||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
|
||||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
|
||||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
|
||||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
|
||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
|
||||||
- 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
|
||||||
|
|||||||
+40
-39
@@ -1,13 +1,48 @@
|
|||||||
name: E2E tests
|
name: E2E
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
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:
|
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-4vcpu-ubuntu-2204
|
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13
|
image: postgres:13
|
||||||
@@ -36,13 +71,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shard: [1, 2, 3, 4, 5]
|
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
|
||||||
@@ -50,40 +85,6 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||||
env:
|
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
|
||||||
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_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
|
||||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
|
||||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
|
||||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
|
||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
name: Production Build
|
name: Integration
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_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_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
|
||||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
|
||||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_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 }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
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_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_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||||
@@ -34,16 +38,11 @@ env:
|
|||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
|
||||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
|
||||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
integration:
|
||||||
name: Build
|
timeout-minutes: 20
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
name: Integration
|
||||||
timeout-minutes: 30
|
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13
|
image: postgres:13
|
||||||
@@ -61,9 +60,31 @@ jobs:
|
|||||||
--health-retries 5
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- 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/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- uses: ./.github/actions/cache-build
|
- 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
|
||||||
|
|||||||
@@ -7,18 +7,49 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
|
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:
|
jobs:
|
||||||
build:
|
|
||||||
name: Production build
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
|
||||||
uses: ./.github/workflows/production-build.yml
|
|
||||||
secrets: inherit
|
|
||||||
analyze:
|
analyze:
|
||||||
needs: build
|
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-4vcpu-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'
|
||||||
+102
-41
@@ -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:
|
||||||
@@ -14,101 +14,162 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
name: Detect changes
|
name: Detect changes
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
permissions:
|
permissions:
|
||||||
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
|
||||||
|
|
||||||
build:
|
unit-test:
|
||||||
name: Production build
|
name: Tests
|
||||||
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/production-build.yml
|
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
|
secrets: inherit
|
||||||
|
|
||||||
build-api-v1:
|
build-api-v1:
|
||||||
name: Production build
|
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-without-database:
|
build-api-v2:
|
||||||
name: Production build (without database)
|
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
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Production builds
|
||||||
|
needs: [changes, check-label]
|
||||||
|
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]
|
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]
|
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]
|
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]
|
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, 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-4vcpu-ubuntu-2204
|
|
||||||
steps:
|
steps:
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
|
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
|
||||||
|
|||||||
@@ -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: |
|
||||||
@@ -39,8 +39,8 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production build
|
name: Production builds
|
||||||
uses: ./.github/workflows/production-build.yml
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
@@ -67,15 +67,10 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build-without-database:
|
|
||||||
name: Production build (without database)
|
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [e2e, e2e-app-store, e2e-embed, e2e-embed-react, build-without-database]
|
needs: [changes, lint, build, e2e, e2e-app-store, e2e-embed, e2e-embed-react]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
name: Production Build (without database)
|
name: Production Builds
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
@@ -37,11 +36,11 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Web App
|
||||||
runs-on: ubuntu-latest
|
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-4vcpu-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:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://go.cal.com/discord"><img src="https://img.shields.io/badge/Discord-go.cal.com%2Fdiscord-%234A154B" alt="Join Cal.com Discord"></a>
|
<a href="https://go.cal.com/discord"><img src="https://img.shields.io/badge/Discord-go.cal.com%2Fdiscord-%234A154B" alt="Join Cal.com Discord"></a>
|
||||||
<a href="https://producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
<a href="https://www.producthunt.com/products/cal-com"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||||
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||||
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
|
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
|
||||||
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
||||||
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
|
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
|
||||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
<a href="https://hub.docker.com/r/calcom/cal.com"><img src="https://img.shields.io/docker/pulls/calcom/cal.com"></a>
|
||||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||||
|
|||||||
@@ -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({
|
||||||
@@ -82,4 +82,5 @@ export const schemaBookingReadPublic = Booking.extend({
|
|||||||
metadata: true,
|
metadata: true,
|
||||||
status: true,
|
status: true,
|
||||||
responses: true,
|
responses: true,
|
||||||
|
fromReschedule: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ export const withValidQueryIdTransformParseInt = withValidation({
|
|||||||
type: "Zod",
|
type: "Zod",
|
||||||
mode: "query",
|
mode: "query",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
|
||||||
|
recordingId: z.string(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
|
|||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
pendingPayment: true,
|
pendingPayment: true,
|
||||||
|
isOrganization: true,
|
||||||
|
isPlatform: true,
|
||||||
|
smsLockState: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const schemaTeamRequiredParams = z.object({
|
const schemaTeamRequiredParams = z.object({
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { minimumTokenResponseSchema } from "@calcom/app-store/_utils/oauth/parseRefreshTokenResponse";
|
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -63,7 +63,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
||||||
);
|
);
|
||||||
|
|
||||||
const key = minimumTokenResponseSchema.parse(decryptedKey);
|
const key = OAuth2UniversalSchema.parse(decryptedKey);
|
||||||
|
|
||||||
const credential = await prisma.credential.update({
|
const credential = await prisma.credential.update({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||||
import { minimumTokenResponseSchema } from "@calcom/app-store/_utils/oauth/parseRefreshTokenResponse";
|
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
@@ -70,7 +70,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
||||||
);
|
);
|
||||||
|
|
||||||
const key = minimumTokenResponseSchema.parse(decryptedKey);
|
const key = OAuth2UniversalSchema.parse(decryptedKey);
|
||||||
|
|
||||||
// Need to get app type
|
// Need to get app type
|
||||||
const app = await prisma.app.findUnique({
|
const app = await prisma.app.findUnique({
|
||||||
|
|||||||
@@ -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: {
|
||||||
@@ -57,6 +88,11 @@ const swaggerHandler = withSwagger({
|
|||||||
type: "string",
|
type: "string",
|
||||||
example: "Europe/London",
|
example: "Europe/London",
|
||||||
},
|
},
|
||||||
|
fromReschedule: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
format: "uuid",
|
||||||
|
},
|
||||||
attendees: {
|
attendees: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user