diff --git a/.github/actions/cache-build/action.yml b/.github/actions/cache-build/action.yml index 17b397cb53..1ef35a7831 100644 --- a/.github/actions/cache-build/action.yml +++ b/.github/actions/cache-build/action.yml @@ -8,7 +8,7 @@ runs: using: "composite" steps: - name: Cache production build - uses: actions/cache@v3 + uses: buildjet/cache@v3 id: cache-build env: cache-name: prod-build diff --git a/.github/actions/cache-db/action.yml b/.github/actions/cache-db/action.yml index a706e3260c..f69fd9513a 100644 --- a/.github/actions/cache-db/action.yml +++ b/.github/actions/cache-db/action.yml @@ -12,7 +12,7 @@ runs: steps: - name: Cache database id: cache-db - uses: actions/cache@v3 + uses: buildjet/cache@v3 env: cache-name: cache-db key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }} diff --git a/.github/actions/env-read-file/action.yml b/.github/actions/env-read-file/action.yml index 26ca4e0306..7caf7328ff 100644 --- a/.github/actions/env-read-file/action.yml +++ b/.github/actions/env-read-file/action.yml @@ -4,7 +4,7 @@ runs: using: "composite" steps: - name: Create env file - uses: actions/cache@v3 + uses: buildjet/cache@v3 id: env-cache with: path: gh.env @@ -13,4 +13,4 @@ runs: - name: Set Environment Variables uses: tw3lveparsecs/github-actions-setvars@latest with: - envFilePath: gh.env \ No newline at end of file + envFilePath: gh.env diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml index 106298bfab..216cdce6de 100644 --- a/.github/actions/yarn-install/action.yml +++ b/.github/actions/yarn-install/action.yml @@ -20,7 +20,7 @@ runs: using: "composite" steps: - name: Use Node ${{ inputs.node_version }} - uses: actions/setup-node@v3 + uses: buildjet/setup-node@v3 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: actions/cache@v3 + uses: buildjet/cache@v3 id: yarn-download-cache with: path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }} @@ -43,7 +43,7 @@ runs: # Invalidated on yarn.lock changes - name: Restore yarn install state id: yarn-install-state-cache - uses: actions/cache@v3 + uses: buildjet/cache@v3 with: path: .yarn/ci-cache/ key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} diff --git a/.github/actions/yarn-playwright-install/action.yml b/.github/actions/yarn-playwright-install/action.yml index 65a093bfe2..1e118e3099 100644 --- a/.github/actions/yarn-playwright-install/action.yml +++ b/.github/actions/yarn-playwright-install/action.yml @@ -5,7 +5,7 @@ runs: steps: - name: Cache playwright binaries id: playwright-cache - uses: actions/cache@v2 + uses: buildjet/cache@v2 with: path: | ~/Library/Caches/ms-playwright diff --git a/.github/workflows/cache-clean.yml b/.github/workflows/cache-clean.yml index 99583c7e8e..dd0be43ec5 100644 --- a/.github/workflows/cache-clean.yml +++ b/.github/workflows/cache-clean.yml @@ -6,7 +6,7 @@ on: jobs: cleanup: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - name: Check out code uses: actions/checkout@v3 diff --git a/.github/workflows/check-types.yml b/.github/workflows/check-types.yml index 1628003b6c..644ca2e9d6 100644 --- a/.github/workflows/check-types.yml +++ b/.github/workflows/check-types.yml @@ -5,7 +5,7 @@ env: NODE_OPTIONS: "--max-old-space-size=8192" jobs: check-types: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout diff --git a/.github/workflows/e2e-app-store.yml b/.github/workflows/e2e-app-store.yml index 8db02bc677..dd1b98953a 100644 --- a/.github/workflows/e2e-app-store.yml +++ b/.github/workflows/e2e-app-store.yml @@ -6,7 +6,7 @@ jobs: e2e-app-store: timeout-minutes: 20 name: E2E App-Store Apps - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 services: postgres: image: postgres:12.1 diff --git a/.github/workflows/e2e-embed-react.yml b/.github/workflows/e2e-embed-react.yml index 5ce70167e0..7b27cb7951 100644 --- a/.github/workflows/e2e-embed-react.yml +++ b/.github/workflows/e2e-embed-react.yml @@ -5,7 +5,7 @@ on: jobs: e2e-embed: timeout-minutes: 20 - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 services: postgres: image: postgres:12.1 diff --git a/.github/workflows/e2e-embed.yml b/.github/workflows/e2e-embed.yml index c60888f9f7..335471cbe6 100644 --- a/.github/workflows/e2e-embed.yml +++ b/.github/workflows/e2e-embed.yml @@ -5,7 +5,7 @@ on: jobs: e2e-embed: timeout-minutes: 20 - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 services: postgres: image: postgres:12.1 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f77708fc25..a8c8dfcc9e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -4,8 +4,8 @@ on: jobs: e2e: timeout-minutes: 20 - name: E2E tests - runs-on: ubuntu-latest + name: E2E tests (${{ matrix.shard }}/${{ strategy.job-total }}) + runs-on: buildjet-4vcpu-ubuntu-2204 services: postgres: image: postgres:12.1 @@ -14,6 +14,10 @@ jobs: POSTGRES_DB: calendso ports: - 5432:5432 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5] steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout @@ -24,12 +28,12 @@ jobs: - uses: ./.github/actions/cache-db - uses: ./.github/actions/cache-build - name: Run Tests - run: yarn test-e2e + run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }} env: DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} - name: Upload Test Results if: ${{ always() }} uses: actions/upload-artifact@v2 with: - name: test-results + name: test-results-${{ matrix.shard }}_${{ strategy.job-total }} path: test-results diff --git a/.github/workflows/env-create-file.yml b/.github/workflows/env-create-file.yml index 79a4073b12..00b229c6da 100644 --- a/.github/workflows/env-create-file.yml +++ b/.github/workflows/env-create-file.yml @@ -20,7 +20,7 @@ env: INPUT_ENV_PAYMENT_FEE_FIXED: 10 INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso INPUT_ENV_SAML_ADMINS: pro@example.com - INPUT_ENV_NEXTAUTH_URL: http://127.0.0.1:3000/api/auth + INPUT_ENV_NEXTAUTH_URL: http://localhost:3000 INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1 # INPUT_ENV_EMAIL_FROM: e2e@cal.com # INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }} @@ -36,14 +36,14 @@ env: jobs: create_env_file: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout - uses: ozaytsev86/create-env-file@v1 with: file-name: ${{ github.workspace }}/gh.env - - uses: actions/cache@v3 + - uses: buildjet/cache@v3 id: env-cache with: path: gh.env diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2c2d5ebc69..b566168d0a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,7 @@ on: workflow_call: jobs: lint: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml index 1e5d821a01..972e8af225 100644 --- a/.github/workflows/nextjs-bundle-analysis.yml +++ b/.github/workflows/nextjs-bundle-analysis.yml @@ -15,7 +15,7 @@ jobs: analyze: needs: build if: always() - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f8961b1755..e42cc70e91 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,7 +16,7 @@ concurrency: jobs: changes: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 permissions: pull-requests: read outputs: @@ -66,6 +66,12 @@ jobs: uses: ./.github/workflows/production-build.yml secrets: inherit + build-without-database: + name: Production build (without database) + needs: env + uses: ./.github/workflows/production-build-without-database.yml + secrets: inherit + e2e: name: E2E tests needs: [changes, lint, build] @@ -101,7 +107,7 @@ jobs: required: needs: [lint, type-check, test, build, e2e] if: always() - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - name: fail if conditional jobs failed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') diff --git a/.github/workflows/production-build-without-database.yml b/.github/workflows/production-build-without-database.yml new file mode 100644 index 0000000000..a55f44e180 --- /dev/null +++ b/.github/workflows/production-build-without-database.yml @@ -0,0 +1,16 @@ +name: Production Build (without database) + +on: + workflow_call: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/dangerous-git-checkout + - uses: ./.github/actions/yarn-install + - uses: ./.github/actions/env-read-file + - uses: ./.github/actions/cache-build diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml index ef75c7ac31..4bcc8ef71f 100644 --- a/.github/workflows/production-build.yml +++ b/.github/workflows/production-build.yml @@ -6,7 +6,7 @@ on: jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 timeout-minutes: 30 services: postgres: diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml new file mode 100644 index 0000000000..5585523eec --- /dev/null +++ b/.github/workflows/semantic-pull-requests.yml @@ -0,0 +1,21 @@ +name: "Validate PRs" + +on: + pull_request_target: + types: + - opened + - reopened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + validate-pr: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e71506689f..fcc7c6ed1f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ on: jobs: test: timeout-minutes: 20 - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout diff --git a/.github/workflows/yarn-install.yml b/.github/workflows/yarn-install.yml index 3ee08b3f48..5fbb5c0f1b 100644 --- a/.github/workflows/yarn-install.yml +++ b/.github/workflows/yarn-install.yml @@ -6,7 +6,7 @@ on: jobs: setup: name: Yarn install & cache - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 timeout-minutes: 10 steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index d7fc9c6244..e87c7d513e 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ To get a local copy up and running, please follow these simple steps. Here is what you need to be able to run Cal.com. -- Node.js (Version: >=15.x <17) +- Node.js (Version: >=18.x) - PostgreSQL - Yarn _(recommended)_ diff --git a/apps/api/.eslintrc.js b/apps/api/.eslintrc.js deleted file mode 100644 index 0e71ec1eae..0000000000 --- a/apps/api/.eslintrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@calcom/config/eslint-preset"); diff --git a/apps/api/lib/validations/booking.ts b/apps/api/lib/validations/booking.ts index 53da5396aa..8298f3cce2 100644 --- a/apps/api/lib/validations/booking.ts +++ b/apps/api/lib/validations/booking.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { _BookingModel as Booking, _AttendeeModel, _UserModel } from "@calcom/prisma/zod"; +import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } from "@calcom/prisma/zod"; import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils"; import { schemaQueryUserId } from "./shared/queryUserId"; @@ -49,6 +49,15 @@ export const schemaBookingReadPublic = Booking.extend({ locale: true, }) .optional(), + payment: z + .array( + _PaymentModel.pick({ + id: true, + success: true, + paymentOption: true, + }) + ) + .optional(), }).pick({ id: true, userId: true, @@ -61,6 +70,7 @@ export const schemaBookingReadPublic = Booking.extend({ timeZone: true, attendees: true, user: true, + payment: true, metadata: true, status: true, responses: true, diff --git a/apps/api/lib/validations/shared/queryAttendeeEmail.ts b/apps/api/lib/validations/shared/queryAttendeeEmail.ts new file mode 100644 index 0000000000..d7919bf53f --- /dev/null +++ b/apps/api/lib/validations/shared/queryAttendeeEmail.ts @@ -0,0 +1,20 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +import { baseApiParams } from "./baseApiParams"; + +// Extracted out as utility function so can be reused +// at different endpoints that require this validation. +export const schemaQueryAttendeeEmail = baseApiParams.extend({ + attendeeEmail: z.string().email(), +}); + +export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({ + attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(), +}); + +export const withValidQueryAttendeeEmail = withValidation({ + schema: schemaQueryAttendeeEmail, + type: "Zod", + mode: "query", +}); diff --git a/apps/api/package.json b/apps/api/package.json index af678fe46b..1a975d25fa 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,10 +11,8 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", "dev": "PORT=3002 next dev", "lint": "eslint . --ignore-path .gitignore", - "lint:report": "eslint . --format json --output-file ../../lint-results/api.json", "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", "start": "PORT=3002 next start", - "test": "echo 'No tests yet' && exit 0", "type-check": "tsc --pretty --noEmit" }, "devDependencies": { diff --git a/apps/api/pages/api/availability/_get.ts b/apps/api/pages/api/availability/_get.ts index 42371e0be7..c8711d6a61 100644 --- a/apps/api/pages/api/availability/_get.ts +++ b/apps/api/pages/api/availability/_get.ts @@ -5,6 +5,7 @@ import { getUserAvailability } from "@calcom/core/getUserAvailability"; import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { availabilityUserSelect } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; /** @@ -97,6 +98,9 @@ import { stringOrNumber } from "@calcom/prisma/zod-utils"; * 404: * description: User not found | Team not found | Team has no members */ +interface MemberRoles { + [userId: number | string]: MembershipRole; +} const availabilitySchema = z .object({ @@ -113,7 +117,7 @@ const availabilitySchema = z ); async function handler(req: NextApiRequest) { - const { prisma, isAdmin } = req; + const { prisma, isAdmin, userId: reqUserId } = req; const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query); if (!teamId) return getUserAvailability({ @@ -134,7 +138,16 @@ async function handler(req: NextApiRequest) { where: { id: { in: allMemberIds } }, select: availabilityUserSelect, }); - if (!isAdmin) throw new HttpError({ statusCode: 403, message: "Forbidden" }); + const memberRoles: MemberRoles = team.members.reduce((acc: MemberRoles, membership) => { + acc[membership.userId] = membership.role; + return acc; + }, {} as MemberRoles); + // check if the user is a team Admin or Owner, if it is a team request, or a system Admin + const isUserAdminOrOwner = + memberRoles[reqUserId] == MembershipRole.ADMIN || + memberRoles[reqUserId] == MembershipRole.OWNER || + isAdmin; + if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" }); const availabilities = members.map(async (user) => { return { userId: user.id, diff --git a/apps/api/pages/api/bookings/[id]/_get.ts b/apps/api/pages/api/bookings/[id]/_get.ts index 5064ba2d78..c549af8b86 100644 --- a/apps/api/pages/api/bookings/[id]/_get.ts +++ b/apps/api/pages/api/bookings/[id]/_get.ts @@ -32,40 +32,65 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform * content: * application/json: * schema: - * $ref: "#/components/schemas/ArrayOfBookings" + * $ref: "#/components/schemas/Booking" * examples: - * bookings: - * value: [ + * booking: + * value: * { - * "id": 1, - * "description": "Meeting with John", - * "eventTypeId": 2, - * "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", - * "title": "Business Meeting", - * "startTime": "2023-04-20T10:00:00.000Z", - * "endTime": "2023-04-20T11:00:00.000Z", - * "timeZone": "Europe/London", - * "attendees": [ - * { - * "email": "example@cal.com", - * "name": "John Doe", - * "timeZone": "Europe/London", + * "booking": { + * "id": 91, + * "userId": 5, + * "description": "", + * "eventTypeId": 7, + * "uid": "bFJeNb2uX8ANpT3JL5EfXw", + * "title": "60min between Pro Example and John Doe", + * "startTime": "2023-05-25T09:30:00.000Z", + * "endTime": "2023-05-25T10:30:00.000Z", + * "attendees": [ + * { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "timeZone": "Asia/Kolkata", + * "locale": "en" + * } + * ], + * "user": { + * "email": "pro@example.com", + * "name": "Pro Example", + * "timeZone": "Asia/Kolkata", * "locale": "en" + * }, + * "payment": [ + * { + * "id": 1, + * "success": true, + * "paymentOption": "ON_BOOKING" + * } + * ], + * "metadata": {}, + * "status": "ACCEPTED", + * "responses": { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "location": { + * "optionValue": "", + * "value": "inPerson" + * } * } - * ] + * } * } - * ] * 401: * description: Authorization information is missing or invalid. * 404: * description: Booking was not found */ + export async function getHandler(req: NextApiRequest) { const { prisma, query } = req; const { id } = schemaQueryIdParseInt.parse(query); const booking = await prisma.booking.findUnique({ where: { id }, - include: { attendees: true, user: true }, + include: { attendees: true, user: true, payment: true }, }); return { booking: schemaBookingReadPublic.parse(booking) }; } diff --git a/apps/api/pages/api/bookings/_get.ts b/apps/api/pages/api/bookings/_get.ts index 006b39472e..9166b80ec9 100644 --- a/apps/api/pages/api/bookings/_get.ts +++ b/apps/api/pages/api/bookings/_get.ts @@ -5,6 +5,7 @@ import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { schemaBookingReadPublic } from "~/lib/validations/booking"; +import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail"; import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; /** @@ -31,6 +32,19 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * items: * type: integer * example: [2, 3, 4] + * - in: query + * name: attendeeEmails + * required: false + * schema: + * oneOf: + * - type: string + * format: email + * example: john.doe@example.com + * - type: array + * items: + * type: string + * format: email + * example: [john.doe@example.com, jane.doe@example.com] * operationId: listBookings * tags: * - bookings @@ -45,22 +59,47 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * bookings: * value: [ * { - * "id": 1, - * "description": "Meeting with John", - * "eventTypeId": 2, - * "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", - * "title": "Business Meeting", - * "startTime": "2023-04-20T10:00:00.000Z", - * "endTime": "2023-04-20T11:00:00.000Z", - * "timeZone": "Europe/London", - * "attendees": [ - * { - * "email": "example@cal.com", - * "name": "John Doe", - * "timeZone": "Europe/London", + * "booking": { + * "id": 91, + * "userId": 5, + * "description": "", + * "eventTypeId": 7, + * "uid": "bFJeNb2uX8ANpT3JL5EfXw", + * "title": "60min between Pro Example and John Doe", + * "startTime": "2023-05-25T09:30:00.000Z", + * "endTime": "2023-05-25T10:30:00.000Z", + * "attendees": [ + * { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "timeZone": "Asia/Kolkata", + * "locale": "en" + * } + * ], + * "user": { + * "email": "pro@example.com", + * "name": "Pro Example", + * "timeZone": "Asia/Kolkata", * "locale": "en" + * }, + * "payment": [ + * { + * "id": 1, + * "success": true, + * "paymentOption": "ON_BOOKING" + * } + * ], + * "metadata": {}, + * "status": "ACCEPTED", + * "responses": { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "location": { + * "optionValue": "", + * "value": "inPerson" + * } * } - * ] + * } * } * ] * 401: @@ -69,26 +108,42 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * description: No bookings were found */ -async function handler(req: NextApiRequest) { - const { userId, isAdmin, prisma } = req; - const args: Prisma.BookingFindManyArgs = {}; - args.include = { - attendees: true, - user: true, - }; - - /** Only admins can query other users */ - if (isAdmin && req.query.userId) { - 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 = { +/** + * Constructs the WHERE clause for Prisma booking findMany operation. + * + * @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee. + * @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls. + * @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it. + * @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array. + * + * @returns An object that represents the WHERE clause for the findMany/findUnique operation. + */ +function buildWhereClause( + userId: number, + attendeeEmails: string[], + userIds: number[] = [], + userEmails: string[] = [] +) { + const filterByAttendeeEmails = attendeeEmails.length > 0; + const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : { userId }; + let whereClause = {}; + if (filterByAttendeeEmails) { + whereClause = { + AND: [ + userFilter, + { + attendees: { + some: { + email: { in: attendeeEmails }, + }, + }, + }, + ], + }; + } else { + whereClause = { OR: [ - { userId: { in: userIds } }, + userFilter, { attendees: { some: { @@ -98,7 +153,45 @@ async function handler(req: NextApiRequest) { }, ], }; - } else if (!isAdmin) { + } + + return { + ...whereClause, + }; +} + +async function handler(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + const args: Prisma.BookingFindManyArgs = {}; + args.include = { + attendees: true, + user: true, + payment: true, + }; + + const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query); + const attendeeEmails = Array.isArray(queryFilterForAttendeeEmails.attendeeEmail) + ? queryFilterForAttendeeEmails.attendeeEmail + : typeof queryFilterForAttendeeEmails.attendeeEmail === "string" + ? [queryFilterForAttendeeEmails.attendeeEmail] + : []; + const filterByAttendeeEmails = attendeeEmails.length > 0; + + /** Only admins can query other users */ + if (isAdmin) { + if (req.query.userId) { + 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(userId, attendeeEmails, [], []); + } + } else { const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -108,20 +201,7 @@ async function handler(req: NextApiRequest) { if (!user) { throw new HttpError({ message: "User not found", statusCode: 500 }); } - args.where = { - OR: [ - { - userId, - }, - { - attendees: { - some: { - email: user.email, - }, - }, - }, - ], - }; + args.where = buildWhereClause(userId, attendeeEmails, [], []); } const data = await prisma.booking.findMany(args); return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) }; diff --git a/apps/api/pages/api/docs.ts b/apps/api/pages/api/docs.ts index 1773d87457..fc320d011b 100644 --- a/apps/api/pages/api/docs.ts +++ b/apps/api/pages/api/docs.ts @@ -98,6 +98,25 @@ const swaggerHandler = withSwagger({ }, }, }, + payment: { + type: Array, + items: { + properties: { + id: { + type: "number", + example: 1, + }, + success: { + type: "boolean", + example: true, + }, + paymentOption: { + type: "string", + example: "ON_BOOKING", + }, + }, + }, + }, }, }, }, diff --git a/apps/web/CHANGELOG.md b/apps/web/CHANGELOG.md index c4c7804863..795250d0df 100644 --- a/apps/web/CHANGELOG.md +++ b/apps/web/CHANGELOG.md @@ -1,5 +1,23 @@ # @calcom/web +## 2.9.4 + +### Patch Changes + +- Updated dependencies + - @calcom/embed-snippet@1.0.9 + - @calcom/embed-react@1.1.1 + - @calcom/embed-core@1.2.1 + +## 2.9.3 + +### Patch Changes + +- Updated dependencies + - @calcom/embed-react@1.1.0 + - @calcom/embed-core@1.2.0 + - @calcom/embed-snippet@1.0.8 + ## 2.7.16 ### Patch Changes diff --git a/apps/web/components/apps/App.tsx b/apps/web/components/apps/App.tsx index 3f866ebee5..2d74b26300 100644 --- a/apps/web/components/apps/App.tsx +++ b/apps/web/components/apps/App.tsx @@ -129,7 +129,10 @@ const Component = ({ className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize"> {categories[0]} {" "} - • {t("published_by", { author })} + •{" "} + + {t("published_by", { author })} + {isTemplate && ( @@ -257,7 +260,7 @@ const Component = ({ )} -

{t("learn_more")}

+

{t("contact")}