Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -8,7 +8,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache production build
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-build
|
||||
env:
|
||||
# WARN: Don't touch this cache key. Currently github.sha refers to the latest commit in main
|
||||
|
||||
@@ -12,7 +12,7 @@ runs:
|
||||
steps:
|
||||
- name: Cache database
|
||||
id: cache-db
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
env:
|
||||
cache-name: cache-db
|
||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||
|
||||
@@ -4,7 +4,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -20,7 +20,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Use Node ${{ inputs.node_version }}
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
- name: Expose yarn config as "$GITHUB_OUTPUT"
|
||||
@@ -32,7 +32,7 @@ runs:
|
||||
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
|
||||
# Yarn cache is also reusable between arch and os.
|
||||
- name: Restore yarn cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
id: yarn-download-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||
@@ -43,7 +43,7 @@ runs:
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore node_modules
|
||||
id: yarn-nm-cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: "**/node_modules/"
|
||||
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
@@ -51,7 +51,7 @@ runs:
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore yarn install state
|
||||
id: yarn-install-state-cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: .yarn/ci-cache/
|
||||
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- name: Cache playwright binaries
|
||||
id: playwright-cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
|
||||
+7
-3
@@ -1,6 +1,10 @@
|
||||
"❗️ migrations":
|
||||
- packages/prisma/migrations/**/migration.sql
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- packages/prisma/migrations/**/migration.sql
|
||||
|
||||
"❗️ .env changes":
|
||||
- .env.example
|
||||
- .env.appStore.example
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .env.example
|
||||
- .env.appStore.example
|
||||
|
||||
@@ -60,11 +60,31 @@ jobs:
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- name: Cache API v1 production build
|
||||
uses: buildjet/cache@v3
|
||||
id: cache-api-v1-build
|
||||
env:
|
||||
cache-name: api-v1-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/api/v1/**.[jt]s', 'apps/api/v1/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
# Ensures production-build.yml will always be fresh
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/api/v1/.next
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
if [ ${{ steps.cache-api-v1-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v1 build. Skipping build."
|
||||
else
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -30,13 +30,31 @@ jobs:
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Cache API v2 production build
|
||||
uses: buildjet/cache@v3
|
||||
id: cache-api-v2-build
|
||||
env:
|
||||
cache-name: api-v2-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/api/v2/**.[jt]s', 'apps/api/v2/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
# Ensures production-build.yml will always be fresh
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
if [ ${{ steps.cache-api-v2-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v2 build. Skipping build."
|
||||
else
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
check-types:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Show info
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
run: |
|
||||
LAST_DAY=$(date -d tomorrow +%d)
|
||||
if [ "$LAST_DAY" == "01" ]; then
|
||||
echo "::set-output name=is_last_day::true"
|
||||
echo "is_last_day=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::set-output name=is_last_day::false"
|
||||
echo "is_last_day=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: cURL request
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- uses: buildjet/cache-delete@v1
|
||||
with:
|
||||
cache_key: ${{ inputs.cache_key }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: E2E App-Store Apps Tests
|
||||
name: E2E App Store Tests
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: E2E tests
|
||||
name: E2E
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
@@ -41,8 +41,8 @@ env:
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
name: E2E tests (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
name: E2E (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
name: Integration
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
jobs:
|
||||
integration:
|
||||
timeout-minutes: 20
|
||||
name: Integration
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- name: Run Tests
|
||||
run: yarn test -- --integrationTestsOnly
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
@@ -12,18 +12,16 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
team-labels:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
@@ -39,7 +37,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -6,7 +6,7 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Run Linting with Reports
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
@@ -20,9 +20,9 @@ jobs:
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -35,8 +35,8 @@ jobs:
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
unit-test:
|
||||
name: Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
@@ -70,29 +70,36 @@ jobs:
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
integration-test:
|
||||
name: Tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/integration-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
name: Tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
name: Tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
name: Tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
name: Tests
|
||||
needs: [changes, lint, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
@@ -106,7 +113,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [changes, lint, type-check, test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [changes, lint, type-check, unit-test, integration-test, 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:
|
||||
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Determine tag"
|
||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main # Always checkout main even for tagged releases
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Unit tests
|
||||
name: Unit
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_run:
|
||||
@@ -6,10 +6,11 @@ on:
|
||||
types: [completed]
|
||||
jobs:
|
||||
test:
|
||||
name: Unit
|
||||
timeout-minutes: 20
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { isAdminGuard } from "../utils/isAdmin";
|
||||
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
||||
|
||||
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
||||
export const dateNotInPast = function (date: Date) {
|
||||
@@ -36,7 +37,10 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
|
||||
// save the user id in the request for later use
|
||||
req.userId = apiKey.userId;
|
||||
// save the isAdmin boolean here for later use
|
||||
req.isAdmin = await isAdminGuard(req);
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
|
||||
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
|
||||
|
||||
await next();
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { NextMiddleware } from "next-api-middleware";
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
export const verifyCredentialSyncEnabled: NextMiddleware = async (req, res, next) => {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
return res.status(403).json({ error: "Only admin API keys can access credential syncing endpoints" });
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
export function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
||||
export function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
|
||||
@@ -1,10 +1,37 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { ScopeOfAdmin } from "./scopeOfAdmin";
|
||||
|
||||
export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
const { userId } = req;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
return user?.role === UserPermissionRole.ADMIN;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
|
||||
if (!user) return { isAdmin: false, scope: null };
|
||||
|
||||
const { role: userRole } = user;
|
||||
if (userRole === UserPermissionRole.ADMIN) return { isAdmin: true, scope: ScopeOfAdmin.SystemWide };
|
||||
|
||||
const orgOwnerOrAdminMemberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
|
||||
|
||||
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
type AccessibleUsersType = {
|
||||
memberUserIds: number[];
|
||||
adminUserId: number;
|
||||
};
|
||||
|
||||
const getAllOrganizationMemberships = async (
|
||||
memberships: {
|
||||
userId: number;
|
||||
role: MembershipRole;
|
||||
teamId: number;
|
||||
}[],
|
||||
orgId: number
|
||||
) => {
|
||||
return memberships.reduce<number[]>((acc, membership) => {
|
||||
if (membership.teamId === orgId) {
|
||||
acc.push(membership.userId);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const getAllAdminMemberships = async (userId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getAllOrganizationMembers = async (organizationId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: organizationId,
|
||||
accepted: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAccessibleUsers = async ({
|
||||
memberUserIds,
|
||||
adminUserId,
|
||||
}: AccessibleUsersType): Promise<number[]> => {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
accepted: true,
|
||||
OR: [
|
||||
{ userId: { in: memberUserIds } },
|
||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
role: true,
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgId = memberships.find((membership) => membership.userId === adminUserId)?.teamId;
|
||||
if (!orgId) return [];
|
||||
|
||||
const allAccessibleMemberUserIds = await getAllOrganizationMemberships(memberships, orgId);
|
||||
const accessibleUserIds = allAccessibleMemberUserIds.filter((userId) => userId !== adminUserId);
|
||||
return accessibleUserIds;
|
||||
};
|
||||
|
||||
export const retrieveOrgScopedAccessibleUsers = async ({ adminId }: { adminId: number }) => {
|
||||
const adminMemberships = await getAllAdminMemberships(adminId);
|
||||
const organizationId = adminMemberships.find((membership) => membership.team.isOrganization)?.team.id;
|
||||
if (!organizationId) return [];
|
||||
|
||||
const allMemberships = await getAllOrganizationMembers(organizationId);
|
||||
return allMemberships.map((membership) => membership.userId);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const ScopeOfAdmin = {
|
||||
SystemWide: "SystemWide",
|
||||
OrgOwnerOrAdmin: "OrgOwnerOrAdmin",
|
||||
} as const;
|
||||
Vendored
+2
-1
@@ -11,7 +11,8 @@ export declare module "next" {
|
||||
method: string;
|
||||
// session: { user: { id: number } };
|
||||
// query: Partial<{ [key: string]: string | string[] }>;
|
||||
isAdmin: boolean;
|
||||
isSystemWideAdmin: boolean;
|
||||
isOrganizationOwnerOrAdmin: boolean;
|
||||
pagination: { take: number; skip: number };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
export async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
// Admin can check any api key
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if user can access the api key
|
||||
const apiKey = await prisma.apiKey.findFirst({
|
||||
where: { id, userId },
|
||||
|
||||
@@ -16,8 +16,8 @@ type CustomNextApiRequest = NextApiRequest & {
|
||||
function handleAdminRequests(req: CustomNextApiRequest) {
|
||||
// To match type safety with runtime
|
||||
if (!hasReqArgs(req)) throw Error("Missing req.args");
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin && req.query.userId) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
req.args.where = { userId: { in: userIds } };
|
||||
@@ -30,8 +30,8 @@ function hasReqArgs(req: CustomNextApiRequest): req is Ensure<CustomNextApiReque
|
||||
}
|
||||
|
||||
async function getHandler(req: CustomNextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
req.args = isAdmin ? {} : { where: { userId } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
req.args = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
// Proof of concept: allowing mutation in exchange of composability
|
||||
handleAdminRequests(req);
|
||||
const data = await prisma.apiKey.findMany(req.args);
|
||||
|
||||
@@ -10,7 +10,7 @@ import prisma from "@calcom/prisma";
|
||||
import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
||||
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
||||
const args: Prisma.ApiKeyCreateArgs = {
|
||||
@@ -24,9 +24,10 @@ async function postHandler(req: NextApiRequest) {
|
||||
},
|
||||
};
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const query = schemaQueryIdParseInt.parse(req.query);
|
||||
// @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin.
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all user bookings, including attendees
|
||||
const attendee = await prisma.attendee.findFirst({
|
||||
where: { id: query.id, booking: { userId } },
|
||||
|
||||
@@ -63,8 +63,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaAttendeeEditBodyParams>) {
|
||||
const { isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const { userId } = req;
|
||||
const { bookingId } = body;
|
||||
if (bookingId) {
|
||||
|
||||
@@ -31,8 +31,8 @@ import { schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
* description: No attendees were found
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.AttendeeFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.AttendeeFindManyArgs = isSystemWideAdmin ? {} : { where: { booking: { userId } } };
|
||||
const data = await prisma.attendee.findMany(args);
|
||||
const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee));
|
||||
if (!attendees) throw new HttpError({ statusCode: 404, message: "No attendees were found" });
|
||||
|
||||
@@ -52,10 +52,10 @@ import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "~/lib/
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaAttendeeCreateBodyParams.parse(req.body);
|
||||
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
const userBooking = await prisma.booking.findFirst({
|
||||
where: { userId, id: body.bookingId },
|
||||
select: { id: true },
|
||||
|
||||
@@ -5,10 +5,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin, query } = req;
|
||||
const { userId, isSystemWideAdmin, query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
/** Admins can skip the ownership verification */
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
/**
|
||||
* There's a caveat here. If the availability exists but the user doesn't own it,
|
||||
* the user will see a 404 error which may or not be the desired behavior.
|
||||
|
||||
@@ -86,8 +86,8 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
||||
const schedule = await prisma.schedule.findFirst({
|
||||
where: { userId, id: data.scheduleId },
|
||||
|
||||
@@ -189,7 +189,7 @@ const availabilitySchema = z
|
||||
);
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { isAdmin, userId: reqUserId } = req;
|
||||
const { isSystemWideAdmin, userId: reqUserId } = req;
|
||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||
if (!teamId)
|
||||
return getUserAvailability({
|
||||
@@ -224,7 +224,7 @@ async function handler(req: NextApiRequest) {
|
||||
const isUserAdminOrOwner =
|
||||
memberRoles[reqUserId] == MembershipRole.ADMIN ||
|
||||
memberRoles[reqUserId] == MembershipRole.OWNER ||
|
||||
isAdmin;
|
||||
isSystemWideAdmin;
|
||||
if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
const availabilities = members.map(async (user) => {
|
||||
return {
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Here we make sure to only return references of the user's own bookings if the user is not an admin.
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all references where the user has bookings
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { id, booking: { userId } },
|
||||
|
||||
@@ -60,12 +60,12 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body, isAdmin, userId } = req;
|
||||
const { query, body, isSystemWideAdmin, userId } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaBookingEditBodyParams.parse(body);
|
||||
/* If user tries to update bookingId, we run extra checks */
|
||||
if (data.bookingId) {
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: data.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
|
||||
@@ -30,8 +30,10 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe
|
||||
* description: No booking references were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { booking: { userId } } };
|
||||
const data = await prisma.bookingReference.findMany(args);
|
||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaBookingCreateBodyParams.parse(req.body);
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: body.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
|
||||
@@ -3,15 +3,33 @@ import type { NextApiRequest } from "next";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin, query } = req;
|
||||
if (isAdmin) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin, query } = req;
|
||||
if (isSystemWideAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
if (isOrganizationOwnerOrAdmin) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (booking) {
|
||||
const bookingUserId = booking.userId;
|
||||
if (bookingUserId) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [bookingUserId],
|
||||
});
|
||||
if (accessibleUsersIds.length > 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
@@ -43,7 +61,7 @@ async function authMiddleware(req: NextApiRequest) {
|
||||
});
|
||||
|
||||
if (!teamBookings) {
|
||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
@@ -109,14 +110,27 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
|
||||
const { isAdmin } = req;
|
||||
if (body.userId && !isAdmin) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||
if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) {
|
||||
// Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
|
||||
if (body.userId && isOrganizationOwnerOrAdmin) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [body.userId],
|
||||
});
|
||||
if (accessibleUsersIds.length === 0) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getRecordingsOfCalVideoByRoomName } from "@calcom/core/videoClient";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/recordings:
|
||||
* get:
|
||||
* summary: Find all Cal video recordings of that booking
|
||||
* operationId: getRecordingsByBookingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which recordings need to be fetched. Recording download link is only valid for 12 hours and you would have to fetch the recordings again to get new download link
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/ArrayOfRecordings"
|
||||
* examples:
|
||||
* recordings:
|
||||
* value:
|
||||
* - id: "ad90a2e7-154f-49ff-a815-5da1db7bf899"
|
||||
* room_name: "0n22w24AQ5ZFOtEKX2gX"
|
||||
* start_ts: 1716215386
|
||||
* status: "finished"
|
||||
* max_participants: 1
|
||||
* duration: 11
|
||||
* share_token: "x94YK-69Gnh7"
|
||||
* download_link: "https://daily-meeting-recordings..."
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${id}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Cal Video reference found with booking id ${booking.id}`,
|
||||
});
|
||||
|
||||
const recordings = await getRecordingsOfCalVideoByRoomName(roomName);
|
||||
|
||||
if (!recordings || !("data" in recordings)) return [];
|
||||
|
||||
const recordingWithDownloadLink = recordings.data.map((recording: RecordingItemSchema) => {
|
||||
return getDownloadLinkOfCalVideoByRecordingId(recording.id)
|
||||
.then((res) => ({
|
||||
...recording,
|
||||
download_link: res?.download_link,
|
||||
}))
|
||||
.catch((err) => ({ ...recording, download_link: null, error: err.message }));
|
||||
});
|
||||
const res = await Promise.all(recordingWithDownloadLink);
|
||||
return res;
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/transcripts:
|
||||
* get:
|
||||
* summary: Find all Cal video transcripts of that booking
|
||||
* operationId: getTranscriptsByBookingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which recordings need to be fetched.
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${id}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Cal Video reference found with booking id ${booking.id}`,
|
||||
});
|
||||
|
||||
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(roomName);
|
||||
|
||||
return transcripts;
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -5,6 +5,11 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import {
|
||||
getAccessibleUsers,
|
||||
retrieveOrgScopedAccessibleUsers,
|
||||
} from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
@@ -108,7 +113,11 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
* 404:
|
||||
* description: No bookings were found
|
||||
*/
|
||||
|
||||
type GetAdminArgsType = {
|
||||
adminDidQueryUserIds?: boolean;
|
||||
requestedUserIds: number[];
|
||||
userId: number;
|
||||
};
|
||||
/**
|
||||
* Constructs the WHERE clause for Prisma booking findMany operation.
|
||||
*
|
||||
@@ -161,12 +170,20 @@ function buildWhereClause(
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
|
||||
export async function handler(req: NextApiRequest) {
|
||||
const {
|
||||
userId,
|
||||
isSystemWideAdmin,
|
||||
isOrganizationOwnerOrAdmin,
|
||||
pagination: { take, skip },
|
||||
} = req;
|
||||
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
|
||||
|
||||
const args: Prisma.BookingFindManyArgs = {};
|
||||
if (req.query.take && req.query.page) {
|
||||
args.take = take;
|
||||
args.skip = skip;
|
||||
}
|
||||
args.include = {
|
||||
attendees: true,
|
||||
user: true,
|
||||
@@ -182,19 +199,32 @@ async function handler(req: NextApiRequest) {
|
||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (isAdmin) {
|
||||
if (req.query.userId) {
|
||||
if (isSystemWideAdmin) {
|
||||
if (req.query.userId || filterByAttendeeEmails) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails);
|
||||
} else if (filterByAttendeeEmails) {
|
||||
args.where = buildWhereClause(null, attendeeEmails, [], []);
|
||||
const requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
|
||||
const systemWideAdminArgs = {
|
||||
adminDidQueryUserIds: !!req.query.userId,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
};
|
||||
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
||||
}
|
||||
} else if (isOrganizationOwnerOrAdmin) {
|
||||
let requestedUserIds = [userId];
|
||||
if (req.query.userId || filterByAttendeeEmails) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
}
|
||||
const orgWideAdminArgs = {
|
||||
adminDidQueryUserIds: !!req.query.userId,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
};
|
||||
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
|
||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
||||
} else {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
@@ -203,7 +233,7 @@ async function handler(req: NextApiRequest) {
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new HttpError({ message: "User not found", statusCode: 500 });
|
||||
throw new HttpError({ message: "User not found", statusCode: 404 });
|
||||
}
|
||||
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||
}
|
||||
@@ -225,4 +255,53 @@ async function handler(req: NextApiRequest) {
|
||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
const handleSystemWideAdminArgs = async ({
|
||||
adminDidQueryUserIds,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
}: GetAdminArgsType) => {
|
||||
if (adminDidQueryUserIds) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: requestedUserIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
|
||||
return { userId, userIds: requestedUserIds, userEmails };
|
||||
}
|
||||
return { userId: null, userIds: [], userEmails: [] };
|
||||
};
|
||||
|
||||
const handleOrgWideAdminArgs = async ({
|
||||
adminDidQueryUserIds,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
}: GetAdminArgsType) => {
|
||||
if (adminDidQueryUserIds) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: requestedUserIds,
|
||||
});
|
||||
|
||||
if (!accessibleUsersIds.length) throw new HttpError({ message: "No User found", statusCode: 404 });
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: accessibleUsersIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
return { userId, userIds: accessibleUsersIds, userEmails };
|
||||
} else {
|
||||
const accessibleUsersIds = await retrieveOrgScopedAccessibleUsers({
|
||||
adminId: userId,
|
||||
});
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: accessibleUsersIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
return { userId, userIds: accessibleUsersIds, userEmails };
|
||||
}
|
||||
};
|
||||
|
||||
export default withMiddleware("pagination")(defaultResponder(handler));
|
||||
|
||||
@@ -4,6 +4,8 @@ import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBooking
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
@@ -204,8 +206,17 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) req.userId = req.body.userId || userId;
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||
if (isSystemWideAdmin) req.userId = req.body.userId || userId;
|
||||
|
||||
if (isOrganizationOwnerOrAdmin) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [req.body.userId || userId],
|
||||
});
|
||||
const [requestedUserId] = accessibleUsersIds;
|
||||
req.userId = requestedUserId || userId;
|
||||
}
|
||||
|
||||
return await handleNewBooking(req, getBookingDataSchemaForApi);
|
||||
}
|
||||
|
||||
@@ -97,9 +97,10 @@ import { schemaConnectedCalendarsReadPublic } from "~/lib/validations/connected-
|
||||
*/
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
|
||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user can access the event type of this input
|
||||
const eventTypeCustomInput = await prisma.eventTypeCustomInput.findFirst({
|
||||
where: { id, eventType: { userId } },
|
||||
|
||||
@@ -29,8 +29,10 @@ import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-c
|
||||
* description: No eventTypeCustomInputs were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.EventTypeCustomInputFindManyArgs = isAdmin ? {} : { where: { eventType: { userId } } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.EventTypeCustomInputFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { eventType: { userId } } };
|
||||
const data = await prisma.eventTypeCustomInput.findMany(args);
|
||||
return { event_type_custom_inputs: data.map((v) => schemaEventTypeCustomInputPublic.parse(v)) };
|
||||
}
|
||||
|
||||
@@ -80,10 +80,10 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { eventTypeId, ...body } = schemaEventTypeCustomInputBodyParams.parse(req.body);
|
||||
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
/* We check that the user has access to the event type he's trying to add a custom input to. */
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: { id: eventTypeId, userId },
|
||||
|
||||
@@ -6,9 +6,9 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
const userEventTypes = await prisma.eventType.findMany({
|
||||
where: { userId },
|
||||
select: { id: true },
|
||||
|
||||
@@ -83,10 +83,10 @@ type UserCredentialType = {
|
||||
};
|
||||
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, query, body } = req;
|
||||
const { userId, isSystemWideAdmin, query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||
const assignedUserId = isSystemWideAdmin ? parsedBody.userId || userId : userId;
|
||||
|
||||
validateIntegrationInput(parsedBody);
|
||||
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||
|
||||
@@ -61,11 +61,11 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, body } = req;
|
||||
const { userId, isSystemWideAdmin, body } = req;
|
||||
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||
await checkPermissions(req, userId);
|
||||
|
||||
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||
const assignedUserId = isSystemWideAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||
|
||||
/* Check if credentialId data matches the ownership and integration passed in */
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
@@ -120,19 +120,20 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, userId: number) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
const body = schemaDestinationCalendarCreateBodyParams.parse(req.body);
|
||||
|
||||
/* Non-admin users can only create destination calendars for themselves */
|
||||
if (!isAdmin && body.userId)
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId */
|
||||
if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
||||
if (isSystemWideAdmin && !body.userId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
||||
/* User should only be able to create for their own destination calendars*/
|
||||
if (!isAdmin && body.eventTypeId) {
|
||||
if (!isSystemWideAdmin && body.eventTypeId) {
|
||||
const ownsEventType = await prisma.eventType.findFirst({ where: { id: body.eventTypeId, userId } });
|
||||
if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
}
|
||||
|
||||
@@ -27,6 +27,37 @@ const swaggerHandler = withSwagger({
|
||||
$ref: "#/components/schemas/Booking",
|
||||
},
|
||||
},
|
||||
ArrayOfRecordings: {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: "#/components/schemas/Recording",
|
||||
},
|
||||
},
|
||||
Recording: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
room_name: {
|
||||
type: "string",
|
||||
},
|
||||
start_ts: {
|
||||
type: "number",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
},
|
||||
max_participants: {
|
||||
type: "number",
|
||||
},
|
||||
duration: {
|
||||
type: "number",
|
||||
},
|
||||
download_link: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
Booking: {
|
||||
properties: {
|
||||
id: {
|
||||
|
||||
@@ -46,9 +46,9 @@ export async function deleteHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
/** Only event type owners can delete it */
|
||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -87,7 +87,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
req: NextApiRequest,
|
||||
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
||||
) {
|
||||
if (req.isAdmin) return true;
|
||||
if (req.isSystemWideAdmin) return true;
|
||||
if (eventType?.teamId) {
|
||||
req.query.teamId = String(eventType.teamId);
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
|
||||
@@ -237,9 +237,9 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
/** Only event type owners can modify it */
|
||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -43,10 +43,10 @@ import getCalLink from "./_utils/getCalLink";
|
||||
* description: No event types were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
const { slug } = schemaQuerySlug.parse(req.query);
|
||||
const shouldUseUserId = !isAdmin || !slug || !!req.query.userId;
|
||||
const shouldUseUserId = !isSystemWideAdmin || !slug || !!req.query.userId;
|
||||
// When user is admin and no query params are provided we should return all event types.
|
||||
// But currently we return only the event types of the user. Not changing this for backwards compatibility.
|
||||
const data = await prisma.eventType.findMany({
|
||||
@@ -74,9 +74,9 @@ async function getHandler(req: NextApiRequest) {
|
||||
};
|
||||
}
|
||||
// TODO: Extract & reuse.
|
||||
function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
||||
function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
|
||||
@@ -265,7 +265,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, body } = req;
|
||||
const { userId, isSystemWideAdmin, body } = req;
|
||||
|
||||
const {
|
||||
hosts = [],
|
||||
@@ -291,7 +291,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
await checkUserMembership(req);
|
||||
}
|
||||
|
||||
if (isAdmin && parsedBody.userId) {
|
||||
if (isSystemWideAdmin && parsedBody.userId) {
|
||||
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
||||
}
|
||||
|
||||
@@ -311,18 +311,18 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
const body = schemaEventTypeCreateBodyParams.parse(req.body);
|
||||
/* Non-admin users can only create event types for themselves */
|
||||
if (!isAdmin && body.userId)
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
if (
|
||||
body.teamId &&
|
||||
!isAdmin &&
|
||||
!(await canUserAccessTeamWithRole(req.userId, isAdmin, body.teamId, {
|
||||
!isSystemWideAdmin &&
|
||||
!(await canUserAccessTeamWithRole(req.userId, isSystemWideAdmin, body.teamId, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
}))
|
||||
)
|
||||
@@ -331,7 +331,7 @@ async function checkPermissions(req: NextApiRequest) {
|
||||
message: "ADMIN required for `teamId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId or teamId */
|
||||
if (isAdmin && !body.userId && !body.teamId)
|
||||
if (isSystemWideAdmin && !body.userId && !body.teamId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ export default async function checkTeamEventEditPermission(
|
||||
req: NextApiRequest,
|
||||
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
|
||||
) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
let userId = req.userId;
|
||||
if (isAdmin && body.userId) {
|
||||
if (isSystemWideAdmin && body.userId) {
|
||||
userId = body.userId;
|
||||
}
|
||||
if (body.teamId) {
|
||||
|
||||
@@ -58,10 +58,10 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
||||
if (!isAdmin && "accepted" in body)
|
||||
if (!isSystemWideAdmin && "accepted" in body)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||
// Only team OWNERS and ADMINS can add other members
|
||||
const membership = await prisma.membership.findFirst({
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { membershipIdSchema } from "~/lib/validations/membership";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { teamId } = membershipIdSchema.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Only team members can modify a membership
|
||||
const membership = await prisma.membership.findFirst({ where: { userId, teamId } });
|
||||
if (!membership) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -43,10 +43,10 @@ export async function deleteHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isAdmin, userId, query } = req;
|
||||
const { isSystemWideAdmin, userId, query } = req;
|
||||
const userId_teamId = membershipIdSchema.parse(query);
|
||||
// Admin User can do anything including deletion of Admin Team Member in any team
|
||||
if (isAdmin) {
|
||||
if (isSystemWideAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,11 +52,11 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: queryUserId, teamId } = membershipIdSchema.parse(req.query);
|
||||
const data = membershipEditBodySchema.parse(req.body);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Only the invited user can accept the invite
|
||||
if ("accepted" in data && queryUserId !== userId)
|
||||
throw new HttpError({
|
||||
|
||||
@@ -46,26 +46,28 @@ async function getHandler(req: NextApiRequest) {
|
||||
* Returns requested users IDs only if admin, otherwise return only current user ID
|
||||
*/
|
||||
function getUserIds(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/** Only admins can query other users */
|
||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isAdmin && req.query.userId) {
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
return userIds;
|
||||
}
|
||||
// Return all memberships for ADMIN, limit to current user to non-admins
|
||||
return isAdmin ? undefined : [userId];
|
||||
return isSystemWideAdmin ? undefined : [userId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns requested teams IDs only if admin
|
||||
*/
|
||||
function getTeamIds(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
/** Only admins can query other teams */
|
||||
if (!isAdmin && req.query.teamId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isAdmin && req.query.teamId) {
|
||||
if (!isSystemWideAdmin && req.query.teamId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.teamId) {
|
||||
const query = schemaQuerySingleOrMultipleTeamIds.parse(req.query);
|
||||
const teamIds = Array.isArray(query.teamId) ? query.teamId : [query.teamId];
|
||||
return teamIds;
|
||||
|
||||
@@ -37,11 +37,11 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const body = membershipCreateBodySchema.parse(req.body);
|
||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
||||
if (!isAdmin && "accepted" in body)
|
||||
if (!isSystemWideAdmin && "accepted" in body)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||
// Only team OWNERS and ADMINS can add other members
|
||||
const membership = await prisma.membership.findFirst({
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user can access the schedule
|
||||
const schedule = await prisma.schedule.findFirst({
|
||||
where: { id, userId },
|
||||
|
||||
@@ -91,8 +91,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaSingleScheduleBodyParams>) {
|
||||
const { isAdmin } = req;
|
||||
if (isAdmin) return;
|
||||
const { isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
if (body.userId) {
|
||||
throw new HttpError({ statusCode: 403, message: "Non admin cannot change the owner of a schedule" });
|
||||
}
|
||||
|
||||
@@ -77,17 +77,17 @@ export const schemaUserIds = z
|
||||
*/
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const args: Prisma.ScheduleFindManyArgs = isAdmin ? {} : { where: { userId } };
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.ScheduleFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
args.include = { availability: true };
|
||||
|
||||
if (!isAdmin && req.query.userId)
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "Unauthorized: Only admins can query other users",
|
||||
});
|
||||
|
||||
if (isAdmin && req.query.userId) {
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
args.where = { userId: { in: userIds } };
|
||||
|
||||
@@ -80,14 +80,14 @@ import { schemaCreateScheduleBodyParams, schemaSchedulePublic } from "~/lib/vali
|
||||
*/
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaCreateScheduleBodyParams.parse(req.body);
|
||||
let args: Prisma.ScheduleCreateArgs = { data: { ...body, userId } };
|
||||
|
||||
/* If ADMIN we create the schedule for selected user */
|
||||
if (isAdmin && body.userId) args = { data: { ...body, userId: body.userId } };
|
||||
if (isSystemWideAdmin && body.userId) args = { data: { ...body, userId: body.userId } };
|
||||
|
||||
if (!isAdmin && body.userId)
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required for `userId`" });
|
||||
|
||||
// We create default availabilities for the schedule
|
||||
|
||||
@@ -5,10 +5,10 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: queryUserId } = selectedCalendarIdSchema.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user requesting is the same as the one being requested
|
||||
if (userId !== queryUserId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
@@ -53,14 +53,15 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, isAdmin } = req;
|
||||
const { query, isSystemWideAdmin } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data };
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
|
||||
@@ -32,13 +32,14 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
* description: No selected calendars were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/* Admin gets all selected calendar by default, otherwise only the user's ones */
|
||||
const args: Prisma.SelectedCalendarFindManyArgs = isAdmin ? {} : { where: { userId } };
|
||||
const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isAdmin && req.query.userId) {
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
args.where = { userId: { in: userIds } };
|
||||
|
||||
@@ -50,13 +50,14 @@ import {
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } };
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
|
||||
@@ -8,10 +8,10 @@ import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { schemaQueryTeamId } from "~/lib/validations/shared/queryTeamId";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
/** Admins can skip the ownership verification */
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
/** Non-members will see a 404 error which may or not be the desired behavior. */
|
||||
await prisma.team.findFirstOrThrow({
|
||||
where: { id: teamId, members: { some: { userId } } },
|
||||
@@ -22,24 +22,24 @@ export async function checkPermissions(
|
||||
req: NextApiRequest,
|
||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||
) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { teamId } = schemaQueryTeamId.parse({
|
||||
teamId: req.query.teamId,
|
||||
version: req.query.version,
|
||||
apiKey: req.query.apiKey,
|
||||
});
|
||||
return canUserAccessTeamWithRole(userId, isAdmin, teamId, role);
|
||||
return canUserAccessTeamWithRole(userId, isSystemWideAdmin, teamId, role);
|
||||
}
|
||||
|
||||
export async function canUserAccessTeamWithRole(
|
||||
userId: number,
|
||||
isAdmin: boolean,
|
||||
isSystemWideAdmin: boolean,
|
||||
teamId: number,
|
||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||
) {
|
||||
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||
if (!isSystemWideAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||
const team = await prisma.team.findFirst(args);
|
||||
if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: ${role.toString()} required` });
|
||||
return team;
|
||||
|
||||
@@ -37,11 +37,11 @@ import { schemaTeamReadPublic } from "~/lib/validations/team";
|
||||
* description: Team was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { isAdmin, userId } = req;
|
||||
const { isSystemWideAdmin, userId } = req;
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
const where: Prisma.TeamWhereInput = { id: teamId };
|
||||
// Non-admins can only query the teams they're part of
|
||||
if (!isAdmin) where.members = { some: { userId } };
|
||||
if (!isSystemWideAdmin) where.members = { some: { userId } };
|
||||
const data = await prisma.team.findFirstOrThrow({ where });
|
||||
return { team: schemaTeamReadPublic.parse(data) };
|
||||
}
|
||||
|
||||
@@ -42,13 +42,13 @@ const querySchema = z.object({
|
||||
* description: No event types were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
|
||||
const { teamId } = querySchema.parse(req.query);
|
||||
|
||||
const args: Prisma.EventTypeFindManyArgs = {
|
||||
where: {
|
||||
team: isAdmin
|
||||
team: isSystemWideAdmin
|
||||
? {
|
||||
id: teamId,
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const patchHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
user: {
|
||||
id: req.userId,
|
||||
username: "" /* Not used in this context */,
|
||||
role: req.isAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER,
|
||||
role: req.isSystemWideAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER,
|
||||
profile: {
|
||||
id: null,
|
||||
organizationId: null,
|
||||
|
||||
@@ -30,10 +30,10 @@ import { schemaTeamsReadPublic } from "~/lib/validations/team";
|
||||
* description: No teams were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const where: Prisma.TeamWhereInput = {};
|
||||
// If user is not ADMIN, return only his data.
|
||||
if (!isAdmin) where.members = { some: { userId } };
|
||||
if (!isSystemWideAdmin) where.members = { some: { userId } };
|
||||
const data = await prisma.team.findMany({ where });
|
||||
return { teams: schemaTeamsReadPublic.parse(data) };
|
||||
}
|
||||
|
||||
@@ -80,12 +80,12 @@ import { schemaTeamCreateBodyParams, schemaTeamReadPublic } from "~/lib/validati
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { body, userId, isAdmin } = req;
|
||||
const { body, userId, isSystemWideAdmin } = req;
|
||||
const { ownerId, ...data } = schemaTeamCreateBodyParams.parse(body);
|
||||
|
||||
await checkPermissions(req);
|
||||
|
||||
const effectiveUserId = isAdmin && ownerId ? ownerId : userId;
|
||||
const effectiveUserId = isSystemWideAdmin && ownerId ? ownerId : userId;
|
||||
|
||||
if (data.slug) {
|
||||
const alreadyExist = await prisma.team.findFirst({
|
||||
@@ -162,11 +162,11 @@ async function postHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
const body = schemaTeamCreateBodyParams.parse(req.body);
|
||||
|
||||
/* Non-admin users can only create teams for themselves */
|
||||
if (!isAdmin && body.ownerId)
|
||||
if (!isSystemWideAdmin && body.ownerId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `ownerId`",
|
||||
|
||||
@@ -38,10 +38,11 @@ import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
const query = schemaQueryUserId.parse(req.query);
|
||||
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
|
||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
if (!isSystemWideAdmin && query.userId !== req.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: query.userId },
|
||||
|
||||
@@ -38,11 +38,12 @@ import { schemaUserReadPublic } from "~/lib/validations/user";
|
||||
* description: User was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
|
||||
const query = schemaQueryUserId.parse(req.query);
|
||||
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
|
||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
if (!isSystemWideAdmin && query.userId !== req.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
const data = await prisma.user.findUnique({ where: { id: query.userId } });
|
||||
const user = schemaUserReadPublic.parse(data);
|
||||
return { user };
|
||||
|
||||
@@ -95,14 +95,15 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
||||
* description: Insufficient permissions to access resource.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
const query = schemaQueryUserId.parse(req.query);
|
||||
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
|
||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
if (!isSystemWideAdmin && query.userId !== req.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
const body = await schemaUserEditBodyParams.parseAsync(req.body);
|
||||
// disable role or branding changes unless admin.
|
||||
if (!isAdmin) {
|
||||
if (!isSystemWideAdmin) {
|
||||
if (body.role) body.role = undefined;
|
||||
if (body.hideBranding) body.hideBranding = undefined;
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ import { schemaUsersReadPublic } from "~/lib/validations/user";
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const {
|
||||
userId,
|
||||
isAdmin,
|
||||
isSystemWideAdmin,
|
||||
pagination: { take, skip },
|
||||
} = req;
|
||||
const where: Prisma.UserWhereInput = {};
|
||||
// If user is not ADMIN, return only his data.
|
||||
if (!isAdmin) where.id = userId;
|
||||
if (!isSystemWideAdmin) where.id = userId;
|
||||
|
||||
if (req.query.email) {
|
||||
const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query);
|
||||
|
||||
@@ -88,9 +88,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const { isSystemWideAdmin } = req;
|
||||
// If user is not ADMIN, return unauthorized.
|
||||
if (!isAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
if (!isSystemWideAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
const data = await schemaUserCreateBodyParams.parseAsync(req.body);
|
||||
const user = await prisma.user.create({ data });
|
||||
req.statusCode = 201;
|
||||
|
||||
@@ -6,10 +6,10 @@ import prisma from "@calcom/prisma";
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isAdmin) return;
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user can access the webhook
|
||||
const webhook = await prisma.webhook.findFirst({
|
||||
where: { id, appId: null, OR: [{ userId }, { eventType: { team: { members: { some: { userId } } } } }] },
|
||||
|
||||
@@ -68,7 +68,7 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, userId, isAdmin } = req;
|
||||
const { query, userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
const {
|
||||
eventTypeId,
|
||||
@@ -80,14 +80,15 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
|
||||
if (eventTypeId) {
|
||||
const where: Prisma.EventTypeWhereInput = { id: eventTypeId };
|
||||
if (!isAdmin) where.userId = userId;
|
||||
if (!isSystemWideAdmin) where.userId = userId;
|
||||
await prisma.eventType.findFirstOrThrow({ where });
|
||||
args.data.eventTypeId = eventTypeId;
|
||||
}
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user