Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc21ab2b0 | ||
|
|
df6ee73e05 | ||
|
|
587af33179 | ||
|
|
3b381b037c | ||
|
|
0968fc56eb | ||
|
|
13044cb5b0 | ||
|
|
580b03f2b9 | ||
|
|
543d4bb6b3 | ||
|
|
21d87d5a80 | ||
|
|
194719e7ba | ||
|
|
dc666fc770 | ||
|
|
50a76e6b27 | ||
|
|
be4d3170db | ||
|
|
7b97002230 | ||
|
|
6319d63e1f | ||
|
|
2e18478b5c | ||
|
|
0ef93edd56 | ||
|
|
d6a3661e58 | ||
|
|
be404436d7 | ||
|
|
b1ebefb854 | ||
|
|
f8aa2701b6 | ||
|
|
034dfc8e6b | ||
|
|
a6b6898697 | ||
|
|
0e5a0bfa7b | ||
|
|
6c01f5eace | ||
|
|
ef2c34c55f | ||
|
|
7c1582c06f | ||
|
|
367e2666c6 | ||
|
|
209a401113 | ||
|
|
837349e3d3 | ||
|
|
e66602aa5c | ||
|
|
082f8e1b04 | ||
|
|
07716474d3 | ||
|
|
f335cc4269 | ||
|
|
f19750cfaf | ||
|
|
46810f69ca | ||
|
|
a7247c594b | ||
|
|
8de7876f72 | ||
|
|
49f75f5070 | ||
|
|
4ff543efc7 | ||
|
|
8af5145b56 | ||
|
|
a31242cdc5 | ||
|
|
e20df1e7aa | ||
|
|
960a368175 | ||
|
|
49cb86dfe9 | ||
|
|
b644ec58af | ||
|
|
bd92b89bf0 | ||
|
|
495ac74255 | ||
|
|
c3d35fd8c8 | ||
|
|
87bc8d649a | ||
|
|
1e9f3a8060 | ||
|
|
bc1d73e9fe | ||
|
|
eca01be58c | ||
|
|
4ad1127184 | ||
|
|
76b9b9b0fe | ||
|
|
ff5c6de5da | ||
|
|
eafb61f515 | ||
|
|
d0ca0dab53 | ||
|
|
e6f561c3d7 | ||
|
|
b7c5cfcf02 | ||
|
|
b1bcbe9dfb | ||
|
|
19e1714794 | ||
|
|
e3e624464d | ||
|
|
dc3c7b43bf | ||
|
|
83736785b7 | ||
|
|
83b6b7caf9 | ||
|
|
6fc3ca539b | ||
|
|
9d58d21e85 | ||
|
|
43f6db194e | ||
|
|
43156fa4d7 | ||
|
|
7eee2728a2 | ||
|
|
861e3aa24f | ||
|
|
517d509321 | ||
|
|
2f9a853023 | ||
|
|
b0125e1a05 | ||
|
|
4daad2ac08 | ||
|
|
9d28f5574d | ||
|
|
be525a27df | ||
|
|
df037b008f | ||
|
|
8fb4df4b8c | ||
|
|
6758654796 | ||
|
|
1e86f1046b | ||
|
|
d3775cd920 | ||
|
|
3ab053bf17 | ||
|
|
4a4bee7140 | ||
|
|
9ba85ea0b2 | ||
|
|
d1d1472f3e | ||
|
|
8d18b3da38 | ||
|
|
631c5f7c53 | ||
|
|
f2ca2bdc83 | ||
|
|
bb36807689 | ||
|
|
7ed32d3c77 | ||
|
|
5d84179345 | ||
|
|
cb437231ad | ||
|
|
d47a5be659 | ||
|
|
e89211782c | ||
|
|
fd454061e7 | ||
|
|
277ce1c244 | ||
|
|
1848cd55b1 | ||
|
|
b0e3cc4dd2 | ||
|
|
b3e344d226 | ||
|
|
810f3b834c | ||
|
|
bf1eacfd9b | ||
|
|
c5c654c879 | ||
|
|
d1ce04edc1 | ||
|
|
1f1862e526 | ||
|
|
63dfc3376c | ||
|
|
3166db9bcf | ||
|
|
f8af9dc17e | ||
|
|
ae659b5176 | ||
|
|
33226f268d | ||
|
|
7e2a0d1a1b | ||
|
|
a385bd4d5f | ||
|
|
4b4bc28f1d | ||
|
|
316fba908b | ||
|
|
d40d282912 | ||
|
|
e81df6fdeb | ||
|
|
76420411b9 | ||
|
|
7ec1617dc2 | ||
|
|
870cadd81f | ||
|
|
04c581a25e | ||
|
|
5ef131333d | ||
|
|
c262220f0f | ||
|
|
b5e08ea80f | ||
|
|
00ee1ef441 | ||
|
|
6cdc9c9f59 | ||
|
|
56d1998f0f | ||
|
|
d59fdf21fd | ||
|
|
1a96149ce3 | ||
|
|
d2bc81758c | ||
|
|
0565e23818 | ||
|
|
34cff214dd | ||
|
|
f7af19b7cb | ||
|
|
6849638b1f | ||
|
|
07969f5287 | ||
|
|
44a6ceff3f | ||
|
|
0e4309777b | ||
|
|
b8f0741923 | ||
|
|
ba1ede2e31 | ||
|
|
b74a49f31d | ||
|
|
31cd77345d | ||
|
|
2be9ff7622 | ||
|
|
96a5abe6c3 | ||
|
|
4cdb8f3bae | ||
|
|
1b111c8add | ||
|
|
be1377e9af | ||
|
|
7226370427 | ||
|
|
524c0d81a7 | ||
|
|
f008a28499 | ||
|
|
381f2781a3 | ||
|
|
6f62c6ad5d | ||
|
|
1545a8ec82 | ||
|
|
58d7e59b38 | ||
|
|
d79a5c3adf | ||
|
|
340b5ab061 | ||
|
|
fa58fe6563 | ||
|
|
251121a76f | ||
|
|
75627cb097 | ||
|
|
f9780fbbd2 | ||
|
|
13649d3bf3 | ||
|
|
65898f29eb | ||
|
|
11267e9359 | ||
|
|
d38b35b626 | ||
|
|
fff521e171 | ||
|
|
fd6666ebad | ||
|
|
f2fa1248eb | ||
|
|
7a9ddf2194 | ||
|
|
a39b5e47a7 | ||
|
|
ae8180e7c2 | ||
|
|
72832354e6 | ||
|
|
df598bf281 |
+10
-1
@@ -17,6 +17,7 @@
|
||||
#
|
||||
|
||||
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
|
||||
CALCOM_LICENSE_KEY=
|
||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||
@@ -192,7 +193,10 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
@@ -285,7 +289,7 @@ NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
|
||||
# use organizations
|
||||
ORGANIZATIONS_ENABLED=
|
||||
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW=37 # $37.00 per seat
|
||||
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
|
||||
# existing organizations based on email domain address
|
||||
@@ -402,6 +406,11 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
|
||||
NEXT_PUBLIC_LOGGER_LEVEL=
|
||||
|
||||
# For storing Daily Video recordings on S3 Bucket
|
||||
CAL_VIDEO_BUCKET_NAME=
|
||||
CAL_VIDEO_BUCKET_REGION=
|
||||
CAL_VIDEO_ASSUME_ROLE_ARN=
|
||||
|
||||
# Used to use Replexica SDK, a tool for real-time AI-powered localization
|
||||
REPLEXICA_API_KEY=
|
||||
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
/apps/api/**/* @calcom/Platform
|
||||
/**/package.json @calcom/Foundation
|
||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
||||
/deploy/**/* @calcom/Foundation
|
||||
/infra/**/* @calcom/Foundation
|
||||
/packages/app-store/applecalendar/**/* @calcom/Foundation
|
||||
@@ -17,6 +18,6 @@
|
||||
/packages/lib/getUserAvailability.ts @calcom/Foundation
|
||||
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
||||
/packages/lib/slots.ts @calcom/Foundation
|
||||
/packages/platform/**/* @calcom/Platform
|
||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||
/packages/prisma/**/* @calcom/Foundation
|
||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||
|
||||
@@ -14,7 +14,7 @@ description: "Run yarn install with node_modules linker and cache enabled"
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v18.x
|
||||
default: v20.x
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
Vendored
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/api-v2"
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -172,24 +172,25 @@ yarn dx
|
||||
```
|
||||
|
||||
#### Development tip
|
||||
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
|
||||
|
||||
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
|
||||
Where {level} can be one of the following:
|
||||
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
|
||||
Where {level} can be one of the following:
|
||||
|
||||
`0` for silly \
|
||||
`1` for trace \
|
||||
`2` for debug \
|
||||
`3` for info \
|
||||
`4` for warn \
|
||||
`5` for error \
|
||||
`6` for fatal
|
||||
`0` for silly \
|
||||
`1` for trace \
|
||||
`2` for debug \
|
||||
`3` for info \
|
||||
`4` for warn \
|
||||
`5` for error \
|
||||
`6` for fatal
|
||||
|
||||
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
|
||||
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
|
||||
|
||||
The logger will include all logs that are at the specified level or higher. For example: \
|
||||
The logger will include all logs that are at the specified level or higher. For example: \
|
||||
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
|
||||
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
|
||||
|
||||
```sh
|
||||
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
|
||||
|
||||
@@ -113,7 +113,7 @@ We use it in several ways, but mainly, we first import the auto-generated schema
|
||||
|
||||
We have some shared validations which several resources require, like baseApiParams which parses apiKey in all requests, or querIdAsString or TransformParseInt which deal with the id's coming from req.query.
|
||||
|
||||
- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elswhere)
|
||||
- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elsewhere)
|
||||
|
||||
- **[*]Public** that also omits any values that we don't want to expose when returning the model as a response, which we parse against before returning all resources.
|
||||
|
||||
@@ -208,7 +208,7 @@ Add this command: `./scripts/vercel-deploy.sh`
|
||||
|
||||
See `scripts/vercel-deploy.sh` for more info on how the deployment is done.
|
||||
|
||||
> _❗ IMORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_
|
||||
> _❗ IMPORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_
|
||||
|
||||
## Environment variables
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
_TeamModel,
|
||||
_UserModel,
|
||||
} from "@calcom/prisma/zod";
|
||||
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
|
||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||
import { extendedBookingCreateBody } from "@calcom/prisma/zod/custom/booking";
|
||||
|
||||
import { schemaQueryUserId } from "./shared/queryUserId";
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ const schemaUserEditParams = z.object({
|
||||
});
|
||||
|
||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end.
|
||||
|
||||
const schemaUserCreateParams = z.object({
|
||||
email: emailSchema.toLowerCase(),
|
||||
@@ -137,7 +137,7 @@ const schemaUserCreateParams = z.object({
|
||||
});
|
||||
|
||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end.
|
||||
export const schemaUserEditBodyParams = schemaUserBaseBodyParams
|
||||
.merge(schemaUserEditParams)
|
||||
.omit({})
|
||||
|
||||
@@ -46,7 +46,7 @@ const nextConfig = {
|
||||
async rewrites() {
|
||||
return {
|
||||
afterFiles: [
|
||||
// This redirects requests recieved at / the root to the /api/ folder.
|
||||
// This redirects requests received at / the root to the /api/ folder.
|
||||
{
|
||||
source: "/v:version/:rest*",
|
||||
destination: "/api/v:version/:rest*",
|
||||
|
||||
@@ -112,7 +112,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
|
||||
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
|
||||
// Organizer has to be a cal user and we can't allow a booking to be transferred to some other cal user's name
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getCalendarCredentialsWithoutDwd, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -185,7 +185,7 @@ async function verifyCredentialsAndGetId({
|
||||
currentCredentialId: number | null;
|
||||
}) {
|
||||
if (parsedBody.integration && parsedBody.externalId) {
|
||||
const calendarCredentials = getCalendarCredentialsWithoutDwd(userCredentials);
|
||||
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendarCredentialsWithoutDwd, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -82,7 +82,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
|
||||
const calendarCredentials = getCalendarCredentialsWithoutDwd(userCredentials);
|
||||
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ async function defaultScheduleId<T extends DefaultScheduleIdEventTypeBase>({
|
||||
}, {} as { [x: number]: number | null });
|
||||
|
||||
return eventTypes.map((eventType) => {
|
||||
// realistically never happens, userId should't be null on personal event types.
|
||||
// realistically never happens, userId shouldn't be null on personal event types.
|
||||
if (!eventType.userId) return eventType;
|
||||
return {
|
||||
...eventType,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { membershipIdSchema } from "~/lib/validations/membership";
|
||||
* - memberships
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, membership removed successfuly
|
||||
* description: OK, membership removed successfully
|
||||
* 400:
|
||||
* description: Bad request. Membership id is invalid.
|
||||
* 401:
|
||||
|
||||
@@ -31,7 +31,7 @@ import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
|
||||
* - users
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, user removed successfuly
|
||||
* description: OK, user removed successfully
|
||||
* 400:
|
||||
* description: Bad request. User id is invalid.
|
||||
* 401:
|
||||
|
||||
@@ -81,6 +81,7 @@ describe("POST /api/selected-calendars", () => {
|
||||
userId: 444444,
|
||||
id: "xxx-xxx",
|
||||
eventTypeId: null,
|
||||
delegationCredentialId: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
@@ -123,6 +124,7 @@ describe("POST /api/selected-calendars", () => {
|
||||
googleChannelResourceId: null,
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
delegationCredentialId: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
eventTypeId: null,
|
||||
error: null,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.25.1
|
||||
ignore: {}
|
||||
# patches apply the minimum changes required to fix a vulnerability
|
||||
patch:
|
||||
'npm:semver:20150403':
|
||||
- '@calcom/platform-libraries-0.0.2 > @calcom/features > @calcom/atoms > rollup-plugin-node-builtins > browserify-fs > levelup > semver':
|
||||
patched: '2025-03-05T12:50:30.654Z'
|
||||
id: 'npm:semver:20150403'
|
||||
path: >-
|
||||
@calcom/platform-libraries-0.0.2 > @calcom/features > @calcom/atoms >
|
||||
rollup-plugin-node-builtins > browserify-fs > levelup > semver
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine as build
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
ARG DATABASE_DIRECT_URL
|
||||
ARG DATABASE_URL
|
||||
|
||||
+30
-1
@@ -54,9 +54,29 @@ CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
|
||||
|
||||
## Running the app
|
||||
|
||||
# Development
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ yarn run start
|
||||
```
|
||||
|
||||
OR if you don't want to use docker, you can run following command.
|
||||
|
||||
```bash
|
||||
$ yarn dev:no-docker
|
||||
```
|
||||
|
||||
Additionally you can run following command(in different terminal) to ensure that any change in any of the dependencies is rebuilt and detected. It watches platform-libraries, platform-constants, platform-enums, platform-utils, platform-types.
|
||||
|
||||
```bash
|
||||
$ yarn run dev:build:watch
|
||||
```
|
||||
|
||||
If you are making changes in packages/platform/libraries, you should run the following command too that would connect your local packages/platform/libraries to the api/v2
|
||||
|
||||
```bash
|
||||
$ yarn local
|
||||
```
|
||||
|
||||
# watch mode
|
||||
$ yarn run start:dev
|
||||
@@ -65,6 +85,9 @@ $ yarn run start:dev
|
||||
$ yarn run start:prod
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
@@ -74,6 +97,12 @@ $ yarn run test
|
||||
# e2e tests
|
||||
$ yarn run test:e2e
|
||||
|
||||
# e2e tests in watch mode
|
||||
$ yarn test:e2e:watch
|
||||
|
||||
# run specific e2e test file in watch mode
|
||||
$ yarn test:e2e:watch --testPathPattern=filePath
|
||||
|
||||
# test coverage
|
||||
$ yarn run test:cov
|
||||
```
|
||||
|
||||
@@ -9,8 +9,14 @@
|
||||
"build": "yarn dev:build && nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build ",
|
||||
"dev:build:watch": "concurrently --names \"libraries,lru-fix,constants,enums,utils,types\" \"yarn _dev:build:watch:libraries\" \"yarn _dev:build:watch:libraries:lru-fix\" \"yarn _dev:build:watch:constants\" \"yarn _dev:build:watch:enums\" \"yarn _dev:build:watch:utils\" \"yarn _dev:build:watch:types\"",
|
||||
"_dev:build:watch:libraries": "yarn workspace @calcom/platform-libraries build:watch",
|
||||
"_dev:build:watch:libraries:lru-fix": "yarn workspace @calcom/platform-libraries watch-lru-fix",
|
||||
"_dev:build:watch:constants": "yarn workspace @calcom/platform-constants build:watch",
|
||||
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
|
||||
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
|
||||
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
|
||||
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
|
||||
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
@@ -24,13 +30,15 @@
|
||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts",
|
||||
"prepare": "yarn run snyk-protect",
|
||||
"snyk-protect": "snyk-protect"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.124",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.151",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -50,10 +58,12 @@
|
||||
"@sentry/nestjs": "^8.37.1",
|
||||
"@sentry/node": "^8.8.0",
|
||||
"@sentry/profiling-node": "^8.37.1",
|
||||
"@snyk/protect": "latest",
|
||||
"body-parser": "^1.20.2",
|
||||
"bull": "^4.12.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.3.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -86,7 +96,6 @@
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/passport-jwt": "^3.0.13",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"jest": "^29.7.0",
|
||||
@@ -99,9 +108,10 @@
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.9.4"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"prisma": {
|
||||
"schema": "../../../packages/prisma/schema.prisma"
|
||||
}
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
X_CAL_PLATFORM_EMBED,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
|
||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||
@@ -76,6 +77,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.useGlobalFilters(new ZodExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalFilters(new TRPCExceptionFilter());
|
||||
app.useGlobalFilters(new CalendarServiceExceptionFilter());
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeysRepository],
|
||||
imports: [
|
||||
PrismaModule,
|
||||
RedisModule,
|
||||
TokensModule,
|
||||
BillingModule,
|
||||
UsersModule,
|
||||
EventTypesModule_2024_04_15,
|
||||
SchedulesModule_2024_04_15,
|
||||
EventTypesModule_2024_06_14,
|
||||
],
|
||||
providers: [
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
OAuthClientRepository,
|
||||
ApiKeysRepository,
|
||||
OAuthClientUsersService,
|
||||
PlatformBookingsService,
|
||||
],
|
||||
controllers: [BookingsController_2024_04_15],
|
||||
})
|
||||
export class BookingsModule_2024_04_15 {}
|
||||
|
||||
@@ -114,7 +114,6 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
optionValue: "",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [],
|
||||
};
|
||||
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
@@ -145,6 +144,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||
expect(responseBody.data.responses).toEqual(bookingResponses);
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
@@ -244,6 +244,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||
expect(responseBody.data.responses).toEqual(bookingResponses);
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
@@ -269,7 +270,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
optionValue: "",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [],
|
||||
guests: ["someone@example.com"],
|
||||
};
|
||||
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
@@ -301,6 +302,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(newBookingMetadata);
|
||||
expect(responseBody.data.responses).toEqual(bookingResponses);
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark
|
||||
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
|
||||
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
|
||||
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
@@ -13,8 +14,10 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -99,7 +102,9 @@ export class BookingsController_2024_04_15 {
|
||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeysRepository
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly platformBookingsService: PlatformBookingsService,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@@ -166,7 +171,8 @@ export class BookingsController_2024_04_15 {
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<Partial<BookingResponse>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const oAuthClientId =
|
||||
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
|
||||
const { orgSlug, locationUrl } = body;
|
||||
req.headers["x-cal-force-slug"] = orgSlug;
|
||||
try {
|
||||
@@ -194,7 +200,7 @@ export class BookingsController_2024_04_15 {
|
||||
async cancelBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() _: CancelBookingInput_2024_04_15,
|
||||
@Body() body: CancelBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
|
||||
@@ -257,11 +263,12 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/recurring")
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateRecurringBookingInput_2024_04_15[],
|
||||
@Body() body: CreateRecurringBookingInput_2024_04_15[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const oAuthClientId =
|
||||
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body[0]?.eventTypeId));
|
||||
try {
|
||||
const recurringEventId = uuidv4();
|
||||
for (const recurringEvent of req.body) {
|
||||
@@ -296,11 +303,12 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/instant")
|
||||
async createInstantBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput_2024_04_15,
|
||||
@Body() body: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const oAuthClientId =
|
||||
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
@@ -338,7 +346,8 @@ export class BookingsController_2024_04_15 {
|
||||
return keyData?.userId;
|
||||
} else {
|
||||
// Access Token
|
||||
return this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
return ownerId;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -346,6 +355,41 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
}
|
||||
|
||||
private async getOwnerIdRescheduledBooking(
|
||||
request: Request,
|
||||
platformClientId?: string
|
||||
): Promise<number | undefined> {
|
||||
if (
|
||||
platformClientId &&
|
||||
request.body.rescheduledBy &&
|
||||
!request.body.rescheduledBy.includes(platformClientId)
|
||||
) {
|
||||
request.body.rescheduledBy = OAuthClientUsersService.getOAuthUserEmail(
|
||||
platformClientId,
|
||||
request.body.rescheduledBy
|
||||
);
|
||||
}
|
||||
|
||||
if (request.body.rescheduledBy) {
|
||||
if (request.body.rescheduledBy !== request.body.responses.email) {
|
||||
return (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getOAuthClientIdFromEventType(eventTypeId: number): Promise<string | undefined> {
|
||||
if (!eventTypeId) {
|
||||
return undefined;
|
||||
}
|
||||
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(eventTypeId);
|
||||
if (!oAuthClientParams) {
|
||||
return undefined;
|
||||
}
|
||||
return oAuthClientParams.platformClientId;
|
||||
}
|
||||
|
||||
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
|
||||
const res = { ...DEFAULT_PLATFORM_PARAMS };
|
||||
|
||||
@@ -379,7 +423,9 @@ export class BookingsController_2024_04_15 {
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const requestId = req.get("X-Request-Id");
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const userId = clone.body.rescheduleUid
|
||||
? await this.getOwnerIdRescheduledBooking(req, oAuthClientId)
|
||||
: await this.getOwnerId(req);
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
@@ -396,9 +442,27 @@ export class BookingsController_2024_04_15 {
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
creationSource: CreationSource.API_V2,
|
||||
};
|
||||
if (oAuthClientId) {
|
||||
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
|
||||
}
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise<void> {
|
||||
if (requestBody?.responses?.email) {
|
||||
requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail(
|
||||
requestBody.responses.email,
|
||||
oAuthClientId
|
||||
);
|
||||
}
|
||||
if (requestBody?.responses?.guests && requestBody?.responses?.guests.length) {
|
||||
requestBody.responses.guests = await this.platformBookingsService.getPlatformAttendeesEmails(
|
||||
requestBody.responses.guests,
|
||||
oAuthClientId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createNextApiRecurringBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
@@ -425,6 +489,9 @@ export class BookingsController_2024_04_15 {
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
creationSource: CreationSource.API_V2,
|
||||
});
|
||||
if (oAuthClientId) {
|
||||
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
|
||||
}
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
|
||||
import {
|
||||
GetBookingsDataEntry,
|
||||
GetBookingsOutput_2024_04_15,
|
||||
} from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import {
|
||||
CreateManagedUserData,
|
||||
CreateManagedUserOutput,
|
||||
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
|
||||
import {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:4321";
|
||||
|
||||
describe("Managed user bookings 2024-04-15", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
const platformAdminEmail = `managed-users-bookings-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
|
||||
const managedUsersTimeZone = "Europe/Rome";
|
||||
const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`;
|
||||
const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`;
|
||||
const thirdManagedUserEmail = `managed-user-bookings-2024-04-15-third-user@api.com`;
|
||||
|
||||
let firstManagedUser: CreateManagedUserData;
|
||||
let secondManagedUser: CreateManagedUserData;
|
||||
let thirdManagedUser: CreateManagedUserData;
|
||||
|
||||
let firstManagedUserEventTypeId: number;
|
||||
|
||||
let firstManagedUserBookingsCount = 0;
|
||||
let secondManagedUserBookingsCount = 0;
|
||||
let thirdManagedUserBookingsCount = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `oauth-client-users-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
isOrganization: true,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd1qwwqeqw-asddsadasd",
|
||||
username: platformAdminEmail,
|
||||
organization: { connect: { id: organization.id } },
|
||||
user: {
|
||||
connect: { id: platformAdmin.id },
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: platformAdmin.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: [CLIENT_REDIRECT_URI],
|
||||
permissions: 1023,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it(`should create first managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: firstManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
firstManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
it("should create an event type for managed user", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Managed user bookings first managed user event type",
|
||||
slug: "managed-user-bookings-first-managed-user-event-type",
|
||||
description: "Managed user bookings first managed user event type description",
|
||||
lengthInMinutes: 30,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
expect(createdEventType.description).toEqual(body.description);
|
||||
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
||||
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
|
||||
firstManagedUserEventTypeId = createdEventType.id;
|
||||
});
|
||||
});
|
||||
|
||||
it(`should create second managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: secondManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Bob Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
secondManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
it(`should create third managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: thirdManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Charlie Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
thirdManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
describe("bookings using original emails", () => {
|
||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: "2040-05-21T09:30:00.000Z",
|
||||
end: "2040-05-21T10:00:00.000Z",
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
metadata: {},
|
||||
hashedLink: "",
|
||||
responses: {
|
||||
name: secondManagedUser.user.name || "unknown-name",
|
||||
email: secondManagedUserEmail,
|
||||
location: {
|
||||
value: "attendeeInPerson",
|
||||
optionValue: "rome",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [],
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getFirstManagedUserBookings.body;
|
||||
const firstManagedUserBookings: GetBookingsDataEntry[] =
|
||||
firstManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getSecondManagedUserBookings.body;
|
||||
const secondManagedUserBookings: GetBookingsDataEntry[] =
|
||||
secondManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
});
|
||||
|
||||
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: "2040-05-21T10:30:00.000Z",
|
||||
end: "2040-05-21T11:00:00.000Z",
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
metadata: {},
|
||||
hashedLink: "",
|
||||
responses: {
|
||||
name: thirdManagedUser.user.name || "unknown-name",
|
||||
email: thirdManagedUserEmail,
|
||||
location: {
|
||||
value: "attendeeInPerson",
|
||||
optionValue: "rome",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [secondManagedUserEmail],
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
thirdManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getFirstManagedUserBookings.body;
|
||||
const firstManagedUserBookings: GetBookingsDataEntry[] =
|
||||
firstManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getSecondManagedUserBookings.body;
|
||||
const secondManagedUserBookings: GetBookingsDataEntry[] =
|
||||
secondManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
|
||||
const getThirdManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getThirdManagedUserBookings.body;
|
||||
const thirdManagedUserBookings: GetBookingsDataEntry[] =
|
||||
thirdManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bookings using OAuth client emails", () => {
|
||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: "2040-05-21T12:00:00.000Z",
|
||||
end: "2040-05-21T12:30:00.000Z",
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
metadata: {},
|
||||
hashedLink: "",
|
||||
responses: {
|
||||
name: secondManagedUser.user.name || "unknown-name",
|
||||
email: secondManagedUser.user.email,
|
||||
location: {
|
||||
value: "attendeeInPerson",
|
||||
optionValue: "rome",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [],
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getFirstManagedUserBookings.body;
|
||||
const firstManagedUserBookings: GetBookingsDataEntry[] =
|
||||
firstManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getSecondManagedUserBookings.body;
|
||||
const secondManagedUserBookings: GetBookingsDataEntry[] =
|
||||
secondManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
});
|
||||
|
||||
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: "2040-05-21T13:00:00.000Z",
|
||||
end: "2040-05-21T13:30:00.000Z",
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
metadata: {},
|
||||
hashedLink: "",
|
||||
responses: {
|
||||
name: thirdManagedUser.user.name || "unknown-name",
|
||||
email: thirdManagedUser.user.email,
|
||||
location: {
|
||||
value: "attendeeInPerson",
|
||||
optionValue: "rome",
|
||||
},
|
||||
notes: "test",
|
||||
guests: [secondManagedUser.user.email],
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
thirdManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getFirstManagedUserBookings.body;
|
||||
const firstManagedUserBookings: GetBookingsDataEntry[] =
|
||||
firstManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getSecondManagedUserBookings.body;
|
||||
const secondManagedUserBookings: GetBookingsDataEntry[] =
|
||||
secondManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
|
||||
const getThirdManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
|
||||
getThirdManagedUserBookings.body;
|
||||
const thirdManagedUserBookings: GetBookingsDataEntry[] =
|
||||
thirdManagedUserBookingsResponseBody.data.bookings;
|
||||
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -8,10 +8,13 @@ import {
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsObject,
|
||||
IsEmail,
|
||||
ValidateNested,
|
||||
isEmail,
|
||||
Validate,
|
||||
} from "class-validator";
|
||||
|
||||
import { RESCHEDULED_BY_DOCS } from "@calcom/platform-types";
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
@@ -27,14 +30,17 @@ class Response {
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@Validate((value: string) => !value || isEmail(value), {
|
||||
message: "Invalid response email",
|
||||
})
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
guests!: string[];
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
guests?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@@ -72,6 +78,13 @@ export class CreateBookingInput_2024_04_15 {
|
||||
@ApiPropertyOptional()
|
||||
rescheduleUid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ description: RESCHEDULED_BY_DOCS })
|
||||
@Validate((value: string) => !value || isEmail(value), {
|
||||
message: "Invalid rescheduledBy email format",
|
||||
})
|
||||
rescheduledBy?: string;
|
||||
|
||||
@IsTimeZone()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
@@ -113,8 +126,9 @@ export class CreateBookingInput_2024_04_15 {
|
||||
@ApiPropertyOptional()
|
||||
seatReferenceUid?: string;
|
||||
|
||||
@Type(() => Response)
|
||||
@ApiProperty({ type: Response })
|
||||
@ValidateNested()
|
||||
@Type(() => Response)
|
||||
responses!: Response;
|
||||
|
||||
@IsString()
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/c
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
import type { AppsStatus } from "@calcom/platform-libraries/app-store";
|
||||
|
||||
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
|
||||
@IsBoolean()
|
||||
|
||||
@@ -160,7 +160,7 @@ class User {
|
||||
email!: string;
|
||||
}
|
||||
|
||||
class GetBookingsDataEntry {
|
||||
export class GetBookingsDataEntry {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@@ -3,12 +3,17 @@ import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controll
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
@@ -18,11 +23,22 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule, BookingSeatModule],
|
||||
imports: [
|
||||
PrismaModule,
|
||||
RedisModule,
|
||||
TokensModule,
|
||||
BillingModule,
|
||||
UsersModule,
|
||||
BookingSeatModule,
|
||||
SchedulesModule_2024_04_15,
|
||||
EventTypesModule_2024_04_15,
|
||||
EventTypesModule_2024_06_14,
|
||||
],
|
||||
providers: [
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
OAuthClientRepository,
|
||||
OAuthClientUsersService,
|
||||
BookingsService_2024_08_13,
|
||||
InputBookingsService_2024_08_13,
|
||||
OutputBookingsService_2024_08_13,
|
||||
@@ -30,6 +46,7 @@ import { Module } from "@nestjs/common";
|
||||
EventTypesRepository_2024_06_14,
|
||||
BookingSeatRepository,
|
||||
ApiKeysRepository,
|
||||
PlatformBookingsService,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import type { Prisma } from "@prisma/client";
|
||||
@Injectable()
|
||||
export class BookingsRepository_2024_08_13 {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
@@ -98,7 +98,7 @@ export class BookingsRepository_2024_08_13 {
|
||||
}
|
||||
|
||||
async getByUidWithAttendeesAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
const booking = await this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
@@ -108,6 +108,15 @@ export class BookingsRepository_2024_08_13 {
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
if (!booking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...booking,
|
||||
responses: booking.responses as Prisma.JsonObject,
|
||||
metadata: booking.metadata as Prisma.JsonObject | null,
|
||||
};
|
||||
}
|
||||
|
||||
async getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid: string) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
@@ -38,11 +39,15 @@ import { Request } from "express";
|
||||
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
CancelBookingInputPipe,
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleBookingInputPipe,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
@@ -64,9 +69,12 @@ import {
|
||||
@DocsTags("Bookings")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-08-13\``,
|
||||
description: `Must be set to ${VERSION_2024_08_13}`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_08_13,
|
||||
},
|
||||
})
|
||||
export class BookingsController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingsController_2024_08_13");
|
||||
@@ -98,7 +106,7 @@ export class BookingsController_2024_08_13 {
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13",
|
||||
"Accepts different types of booking input: Create Booking (Option 1), Create Instant Booking (Option 2), or Create Recurring Booking (Option 3)",
|
||||
})
|
||||
@ApiExtraModels(
|
||||
CreateBookingInput_2024_08_13,
|
||||
@@ -171,9 +179,19 @@ export class BookingsController_2024_08_13 {
|
||||
@UseGuards(BookingUidGuard)
|
||||
@ApiOperation({
|
||||
summary: "Reschedule a booking",
|
||||
description:
|
||||
"Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.",
|
||||
description: "Reschedule a booking or seated booking",
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(RescheduleBookingInput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
|
||||
})
|
||||
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
|
||||
async rescheduleBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body(new RescheduleBookingInputPipe())
|
||||
@@ -198,6 +216,17 @@ export class BookingsController_2024_08_13 {
|
||||
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(CancelBookingInput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(CancelSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
|
||||
})
|
||||
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
|
||||
async cancelBooking(
|
||||
@Req() request: Request,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@@ -335,4 +364,22 @@ export class BookingsController_2024_08_13 {
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/calendar-links")
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@Permissions([BOOKING_READ])
|
||||
@ApiOperation({
|
||||
summary: "Get 'Add to Calendar' links for a booking",
|
||||
description:
|
||||
"Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.",
|
||||
})
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getCalendarLinks(@Param("bookingUid") bookingUid: string): Promise<CalendarLinksOutput_2024_08_13> {
|
||||
const calendarLinks = await this.bookingsService.getCalendarLinks(bookingUid);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: calendarLinks,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
OrganizerRescheduledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
OrganizerCancelledEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/emails";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
@@ -189,7 +189,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
createdBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -238,7 +238,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
rescheduledBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -451,7 +451,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.bookingFieldsResponses.name).toEqual(splitName);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -476,7 +476,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -536,7 +536,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.bookingFieldsResponses["video-url"]).toEqual("http://video-url.com");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Calendar Links", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = `calendar-links-2024-08-13-user-${randomString()}@api.com`;
|
||||
let user: User;
|
||||
let booking: Booking;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `confirm-bookings-2024-08-13-organization-${randomString()}`,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `calendar-links-2024-08-13-${randomString()}-schedule`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `calendar-links-2024-08-13-${randomString()}-event-type`,
|
||||
slug: `calendar-links-2024-08-13-${randomString()}-event-type-slug`,
|
||||
length: 60,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
booking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2020, 0, 8, 12, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
|
||||
uid: `calendar-links-2024-08-13-booking-${randomString()}`,
|
||||
title: "Test Booking",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: event.id,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "johndoe@example.com",
|
||||
name: "John Doe",
|
||||
locale: "en",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
create: {
|
||||
name: "John Doe",
|
||||
email: "johndoe@example.com",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should get calendar links for a booking", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${booking.uid}/calendar-links`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
expect(response.body.data).toBeDefined();
|
||||
|
||||
const googleCalendarLink = response.body.data.find((item: { id: string }) => item.id === 'googleCalendar').link;
|
||||
const microsoftOfficeLink = response.body.data.find((item: { id: string }) => item.id === 'microsoftOffice').link;
|
||||
const microsoftOutlookLink = response.body.data.find((item: { id: string }) => item.id === 'microsoftOutlook').link;
|
||||
const icsLink = response.body.data.find((item: { id: string }) => item.id === 'ics').link;
|
||||
|
||||
expect(googleCalendarLink).toMatch(/^https:\/\/calendar\.google\.com\//);
|
||||
expect(microsoftOfficeLink).toMatch(/^https:\/\/outlook\.office\.com\//);
|
||||
expect(microsoftOutlookLink).toMatch(/^https:\/\/outlook\.live\.com\//);
|
||||
expect(icsLink).toMatch(/^data:text\/calendar,/);
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent booking", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/non-existent-uid/calendar-links`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -174,7 +174,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(dbBooking?.status).toEqual("ACCEPTED");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -235,7 +235,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(dbBooking?.status).toEqual("REJECTED");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+175
-117
@@ -34,7 +34,7 @@ import {
|
||||
AttendeeRequestEmail,
|
||||
OrganizerRequestEmail,
|
||||
AttendeeDeclinedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/emails";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
@@ -94,6 +94,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
let emailsDisabledSetup: EmailSetup;
|
||||
|
||||
const authEmail = `confirm-emails-2024-08-13-admin-${randomString()}@api.com`;
|
||||
let userEmailsEnabled = "";
|
||||
const userEmailsDisabled = `confirm-emails-2024-08-13-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -141,8 +143,12 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
async function setupEnabledEmails() {
|
||||
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
|
||||
|
||||
userEmailsEnabled = `confirm-emails-2024-08-13-user-${randomString()}+${
|
||||
oAuthClientEmailsEnabled.id
|
||||
}@api.com`;
|
||||
|
||||
const user = await userRepositoryFixture.create({
|
||||
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
|
||||
email: userEmailsEnabled,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsEnabled.id,
|
||||
@@ -179,7 +185,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
|
||||
|
||||
const user = await userRepositoryFixture.create({
|
||||
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
|
||||
email: userEmailsDisabled,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsDisabled.id,
|
||||
@@ -265,7 +271,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -282,9 +288,6 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -322,7 +325,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -338,10 +341,6 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeDeclinedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -351,117 +350,176 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
describe("confirming booking that requires confirmation and rescheduling it", () => {
|
||||
it("should send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when confirming a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rescheduling emails", () => {
|
||||
it("should send confirmation emails when organizer reschedules a booking that requires confirmation", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 14, 30, 0)).toISOString(),
|
||||
rescheduledBy: userEmailsEnabled,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("accepted");
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
|
||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send requested rescheduling emails when attendee rescheduling a booking that requires confirmation", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.rescheduledBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when confirming a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
describe("declining booking that requires confirmation", () => {
|
||||
it("should send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
it("should send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when declining a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
it("should send an email when declining a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+5
-5
@@ -36,7 +36,7 @@ import {
|
||||
AttendeeCancelledEmail,
|
||||
OrganizerReassignedEmail,
|
||||
AttendeeUpdatedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/emails";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
@@ -570,7 +570,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
emailsDisabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -610,7 +610,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -787,7 +787,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
emailsEnabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -827,7 +827,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@ import {
|
||||
AttendeeRescheduledEmail,
|
||||
OrganizerCancelledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/emails";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
@@ -261,7 +261,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
|
||||
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -381,7 +381,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+489
@@ -0,0 +1,489 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import {
|
||||
CreateManagedUserData,
|
||||
CreateManagedUserOutput,
|
||||
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
VERSION_2024_08_13,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
ApiSuccessResponse,
|
||||
BookingOutput_2024_08_13,
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:4321";
|
||||
|
||||
describe("Managed user bookings 2024-08-13", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
|
||||
const managedUsersTimeZone = "Europe/Rome";
|
||||
const firstManagedUserEmail = `managed-user-bookings-2024-08-13-first-user-${randomString()}@api.com`;
|
||||
const secondManagedUserEmail = `managed-user-bookings-2024-08-13-second-user-${randomString()}@api.com`;
|
||||
const thirdManagedUserEmail = `managed-user-bookings-2024-08-13-third-user-${randomString()}@api.com`;
|
||||
|
||||
let firstManagedUser: CreateManagedUserData;
|
||||
let secondManagedUser: CreateManagedUserData;
|
||||
let thirdManagedUser: CreateManagedUserData;
|
||||
|
||||
let firstManagedUserEventTypeId: number;
|
||||
|
||||
let firstManagedUserBookingsCount = 0;
|
||||
let secondManagedUserBookingsCount = 0;
|
||||
let thirdManagedUserBookingsCount = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `oauth-client-users-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
isOrganization: true,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd1qwwqeqw-asddsadasd",
|
||||
username: platformAdminEmail,
|
||||
organization: { connect: { id: organization.id } },
|
||||
user: {
|
||||
connect: { id: platformAdmin.id },
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: platformAdmin.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: [CLIENT_REDIRECT_URI],
|
||||
permissions: 1023,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it(`should create first managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: firstManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
firstManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
it("should create an event type for managed user", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Managed user bookings first managed user event type",
|
||||
slug: "managed-user-bookings-first-managed-user-event-type",
|
||||
description: "Managed user bookings first managed user event type description",
|
||||
lengthInMinutes: 30,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
expect(createdEventType.description).toEqual(body.description);
|
||||
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
||||
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
|
||||
firstManagedUserEventTypeId = createdEventType.id;
|
||||
});
|
||||
});
|
||||
|
||||
it(`should create second managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: secondManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Bob Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
secondManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
it(`should create third managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: thirdManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Charlie Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
thirdManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
describe("bookings using original emails", () => {
|
||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
attendee: {
|
||||
name: secondManagedUser.user.name!,
|
||||
email: secondManagedUserEmail,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
language: secondManagedUser.user.locale,
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getFirstManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getSecondManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
|
||||
secondManagedUserBookingsResponseBody.data;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
});
|
||||
|
||||
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
attendee: {
|
||||
name: thirdManagedUser.user.name!,
|
||||
email: thirdManagedUserEmail,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
language: secondManagedUser.user.locale,
|
||||
},
|
||||
guests: [secondManagedUserEmail],
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
thirdManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getFirstManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getSecondManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
|
||||
secondManagedUserBookingsResponseBody.data;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
|
||||
const getThirdManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getThirdManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
|
||||
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bookings using OAuth client emails", () => {
|
||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
attendee: {
|
||||
name: secondManagedUser.user.name!,
|
||||
email: secondManagedUser.user.email,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
language: secondManagedUser.user.locale,
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getFirstManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getSecondManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
|
||||
secondManagedUserBookingsResponseBody.data;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
});
|
||||
|
||||
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 30, 0)).toISOString(),
|
||||
eventTypeId: firstManagedUserEventTypeId,
|
||||
attendee: {
|
||||
name: thirdManagedUser.user.name!,
|
||||
email: thirdManagedUser.user.email,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
language: secondManagedUser.user.locale,
|
||||
},
|
||||
guests: [secondManagedUser.user.email],
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
firstManagedUserBookingsCount += 1;
|
||||
secondManagedUserBookingsCount += 1;
|
||||
thirdManagedUserBookingsCount += 1;
|
||||
|
||||
const getFirstManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getFirstManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
|
||||
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
|
||||
|
||||
const getSecondManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getSecondManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
|
||||
secondManagedUserBookingsResponseBody.data;
|
||||
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
|
||||
|
||||
const getThirdManagedUserBookings = await request(app.getHttpServer())
|
||||
.get("/v2/bookings")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
|
||||
getThirdManagedUserBookings.body;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
|
||||
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -21,7 +21,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries";
|
||||
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries/emails";
|
||||
import {
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
@@ -684,7 +684,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel all remaning recurrences", async () => {
|
||||
it("should cancel all remaining recurrences", async () => {
|
||||
const recurringBookingUid = recurringBooking[0].recurringBookingUid;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
|
||||
describe("Seated bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
@@ -330,7 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -383,6 +385,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -418,6 +424,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -797,7 +807,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -850,6 +860,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -885,6 +899,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -910,7 +928,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -966,6 +984,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
@@ -982,6 +1004,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -1017,6 +1043,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
@@ -1030,6 +1060,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
|
||||
@@ -401,7 +401,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -455,7 +455,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -515,7 +515,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -27,10 +28,12 @@ import { withApiAuth } from "test/utils/withApiAuth";
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
VERSION_2024_08_13,
|
||||
X_CAL_CLIENT_ID,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
@@ -95,6 +98,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `user-bookings-organization-${randomString()}`,
|
||||
isOrganization: true,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
@@ -146,7 +150,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: "booking-in-the-past",
|
||||
uid: `booking-in-the-past-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
@@ -326,6 +330,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
@@ -335,7 +340,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
@@ -389,6 +394,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
email: body.attendee.email,
|
||||
...body.bookingFieldsResponses,
|
||||
guests: body.guests,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
});
|
||||
expect(data.guests).toEqual(body.guests);
|
||||
|
||||
@@ -407,7 +416,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
createdBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -547,7 +556,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.createdAt).toEqual(createdBooking.createdAt);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -607,7 +616,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -972,6 +981,64 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createdAt filter", () => {
|
||||
it("should should get bookings after specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterCreatedAt=${createdRecurringBooking[1].createdAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings before specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?beforeCreatedAt=${createdRecurringBooking[0].createdAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings in createdAt range", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/bookings?afterCreatedAt=${createdRecurringBooking[0].createdAt}&beforeCreatedAt=${createdRecurringBooking[1].createdAt}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by start in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`)
|
||||
@@ -1480,7 +1547,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.cancelledByEmail).toEqual(cancelledByEmail);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1530,13 +1597,358 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.rescheduledByEmail).toEqual(rescheduledByEmail);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("booking location", () => {
|
||||
const address = "123 Main St";
|
||||
const link = "https://cal.com/join/123456";
|
||||
const phone = "+37121999999";
|
||||
|
||||
let eventTypeWithAllLocationsId: number;
|
||||
it("can create event type with all locations except google meet", async () => {
|
||||
const eventTypeBody: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "book using any location",
|
||||
slug: "book-using-any-location",
|
||||
lengthInMinutes: 15,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
address,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
link,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
phone,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "attendeeAddress",
|
||||
},
|
||||
{
|
||||
type: "attendeePhone",
|
||||
},
|
||||
{
|
||||
type: "attendeeDefined",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const eventTypeResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(eventTypeBody)
|
||||
.expect(201);
|
||||
const eventTypeResponseBody: CreateEventTypeOutput_2024_06_14 = eventTypeResponse.body;
|
||||
const createdEventType = eventTypeResponseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.locations).toHaveLength(7);
|
||||
expect(createdEventType.locations).toEqual(createdEventType.locations);
|
||||
eventTypeWithAllLocationsId = createdEventType.id;
|
||||
});
|
||||
|
||||
it("can book with cal video location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(
|
||||
createdBooking.location?.startsWith("http") || createdBooking.location === "integrations:daily"
|
||||
).toEqual(true);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with address location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "address",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(address);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with link location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "link",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(link);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with link location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "phone",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(phone);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with attendeeAddress location", async () => {
|
||||
const attendeeAddress = "123 Example St, City, Valhalla";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeeAddress",
|
||||
address: attendeeAddress,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeeAddress);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
xit("can book with attendeeAddress location", async () => {
|
||||
const attendeePhone = "+37120993151";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeePhone",
|
||||
phone: attendeePhone,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeePhone);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with attendeeDefined location", async () => {
|
||||
const attendeeDefinedLocation = "namek 100";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeeDefined",
|
||||
location: attendeeDefinedLocation,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeeDefinedLocation);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can't book with not location that is not in event type", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "integration",
|
||||
integration: "google-meet",
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("can't book with invalid location type", async () => {
|
||||
const bookingBody = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "blablabala",
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||
return (
|
||||
!Array.isArray(data) &&
|
||||
|
||||
+2
-2
@@ -194,7 +194,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.duration).toEqual(variableLengthEventType.length);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -239,7 +239,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.duration).toEqual(lengthInMinutes);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum, IsString } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class CalendarLink {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "The label of the calendar link",
|
||||
})
|
||||
label!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "The link to the calendar",
|
||||
})
|
||||
link!: string;
|
||||
}
|
||||
|
||||
export class CalendarLinksOutput_2024_08_13 {
|
||||
@ApiProperty({
|
||||
description: "The status of the request, always 'success' for successful responses",
|
||||
example: SUCCESS_STATUS,
|
||||
})
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Calendar links for the booking",
|
||||
type: [CalendarLink],
|
||||
})
|
||||
data!: CalendarLink[];
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
@@ -14,6 +16,7 @@ import { z } from "zod";
|
||||
|
||||
import {
|
||||
handleNewRecurringBooking,
|
||||
getTranslation,
|
||||
getAllUserBookings,
|
||||
handleInstantMeeting,
|
||||
handleCancelBooking,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
roundRobinManualReassignment,
|
||||
handleMarkNoShow,
|
||||
confirmBookingHandler,
|
||||
getCalendarLinks,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import {
|
||||
@@ -67,7 +71,8 @@ export class BookingsService_2024_08_13 {
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly usersRepository: UsersRepository
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly platformBookingsService: PlatformBookingsService
|
||||
) {}
|
||||
|
||||
async createBooking(request: Request, body: CreateBookingInput) {
|
||||
@@ -224,8 +229,7 @@ export class BookingsService_2024_08_13 {
|
||||
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
|
||||
bookingListingByStatus: queryParams.status || [],
|
||||
skip: queryParams.skip ?? 0,
|
||||
// note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason
|
||||
take: queryParams.take ? queryParams.take - 1 : 100,
|
||||
take: queryParams.take ?? 100,
|
||||
filters: this.inputService.transformGetBookingsFilters(queryParams),
|
||||
ctx: {
|
||||
user,
|
||||
@@ -368,7 +372,7 @@ export class BookingsService_2024_08_13 {
|
||||
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
|
||||
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
|
||||
const platformClientParams = bookingBefore?.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
|
||||
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
await handleMarkNoShow({
|
||||
@@ -432,7 +436,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
@@ -471,7 +475,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
@@ -498,7 +502,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
@@ -526,7 +530,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
@@ -547,4 +551,34 @@ export class BookingsService_2024_08_13 {
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
}
|
||||
|
||||
async getCalendarLinks(bookingUid: string): Promise<CalendarLink[]> {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||
}
|
||||
|
||||
if (!booking.eventTypeId) {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(
|
||||
booking.eventTypeId
|
||||
);
|
||||
if (!eventType) {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
// TODO: Maybe we should get locale from query params?
|
||||
return getCalendarLinks({
|
||||
booking,
|
||||
eventType: {
|
||||
...eventType,
|
||||
// TODO: Support dynamic event bookings later. It would require a slug input it seems
|
||||
isDynamic: false,
|
||||
},
|
||||
// It can be made customizable through the API endpoint later.
|
||||
t: await getTranslation("en", "common"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,30 @@ import {
|
||||
bookingResponsesSchema,
|
||||
seatedBookingDataSchema,
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { CreationSource } from "@prisma/client";
|
||||
import { isURL, isPhoneNumber } from "class-validator";
|
||||
import { Request } from "express";
|
||||
import { DateTime } from "luxon";
|
||||
import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
apiToInternalintegrationsMapping,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
@@ -33,6 +40,7 @@ import {
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input";
|
||||
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
@@ -70,71 +78,48 @@ export class InputBookingsService_2024_08_13 {
|
||||
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository
|
||||
private readonly bookingSeatRepository: BookingSeatRepository,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly platformBookingsService: PlatformBookingsService,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
async createBookingRequest(
|
||||
request: Request,
|
||||
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
|
||||
): Promise<BookingRequest> {
|
||||
const bodyTransformed = await this.transformInputCreateBooking(body);
|
||||
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
|
||||
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
|
||||
const bodyTransformed = await this.transformInputCreateBooking(body, oAuthClientParams?.platformClientId);
|
||||
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
this.logger.log(`createBookingRequest_2024_04_15`, {
|
||||
this.logger.log(`createBookingRequest_2024_08_13`, {
|
||||
requestId: request.get("X-Request-Id"),
|
||||
ownerId: userId,
|
||||
location,
|
||||
oAuthClientParams,
|
||||
});
|
||||
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams });
|
||||
newRequest.body = {
|
||||
...bodyTransformed,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
creationSource: CreationSource.API_V2,
|
||||
};
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
|
||||
}
|
||||
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
async getOAuthClientParams(eventTypeId: number) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||
|
||||
let oAuthClient: PlatformOAuthClient | null = null;
|
||||
if (eventType?.userId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
|
||||
} else if (eventType?.teamId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
|
||||
}
|
||||
|
||||
if (oAuthClient) {
|
||||
return {
|
||||
platformClientId: oAuthClient.id,
|
||||
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
|
||||
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
||||
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
||||
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) {
|
||||
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13, platformClientId?: string) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
|
||||
inputBooking.eventTypeId
|
||||
);
|
||||
@@ -151,7 +136,21 @@ export class InputBookingsService_2024_08_13 {
|
||||
);
|
||||
const endTime = startTime.plus({ minutes: lengthInMinutes });
|
||||
|
||||
const guests = inputBooking.guests;
|
||||
const guests =
|
||||
inputBooking.guests && platformClientId
|
||||
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
|
||||
: inputBooking.guests;
|
||||
const attendeeEmail =
|
||||
inputBooking.attendee.email && platformClientId
|
||||
? await this.platformBookingsService.getPlatformAttendeeEmail(
|
||||
inputBooking.attendee.email,
|
||||
platformClientId
|
||||
)
|
||||
: inputBooking.attendee.email;
|
||||
|
||||
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
|
||||
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
|
||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||
|
||||
return {
|
||||
start: startTime.toISO(),
|
||||
@@ -163,20 +162,14 @@ export class InputBookingsService_2024_08_13 {
|
||||
hasHashedBookingLink: false,
|
||||
guests,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: inputBooking.bookingFieldsResponses
|
||||
? {
|
||||
...inputBooking.bookingFieldsResponses,
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
}
|
||||
: {
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
},
|
||||
responses: {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
name: inputBooking.attendee.name,
|
||||
email: attendeeEmail ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
location,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,24 +196,24 @@ export class InputBookingsService_2024_08_13 {
|
||||
request: Request,
|
||||
body: CreateRecurringBookingInput_2024_08_13
|
||||
): Promise<BookingRequest> {
|
||||
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
|
||||
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
|
||||
const bodyTransformed = await this.transformInputCreateRecurringBooking(body);
|
||||
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
|
||||
const bodyTransformed = await this.transformInputCreateRecurringBooking(
|
||||
body,
|
||||
oAuthClientParams?.platformClientId
|
||||
);
|
||||
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, {
|
||||
userId,
|
||||
...oAuthClientParams,
|
||||
platformBookingLocation: location,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
});
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId });
|
||||
}
|
||||
|
||||
newRequest.body = bodyTransformed.map((event) => ({
|
||||
@@ -231,7 +224,148 @@ export class InputBookingsService_2024_08_13 {
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) {
|
||||
transformLocation(location: string | BookingInputLocation_2024_08_13): {
|
||||
value: string;
|
||||
optionValue: string;
|
||||
} {
|
||||
if (typeof location === "string") {
|
||||
// note(Lauris): this is for backwards compatibility because before switching to booking location objects
|
||||
// we only received a string. If someone is complaining that their location is not displaying as a URL
|
||||
// or whatever check that they are not providing a string for bookign location but one of the input objects.
|
||||
if (isURL(location, { require_protocol: false }) || location.startsWith("www.")) {
|
||||
return {
|
||||
value: "link",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
if (isPhoneNumber(location)) {
|
||||
return {
|
||||
value: "phone",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: "somewhereElse",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "integration") {
|
||||
const integration = apiToInternalintegrationsMapping[location.integration];
|
||||
if (!integration) {
|
||||
throw new BadRequestException(`Invalid integration: ${location.integration}`);
|
||||
}
|
||||
return {
|
||||
value: integration,
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "address") {
|
||||
return {
|
||||
value: "inPerson",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeeAddress") {
|
||||
return {
|
||||
value: "attendeeInPerson",
|
||||
optionValue: location.address,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "link") {
|
||||
return {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "phone") {
|
||||
return {
|
||||
value: "userPhone",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "organizersDefaultApp") {
|
||||
return {
|
||||
value: "conferencing",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeePhone") {
|
||||
return {
|
||||
value: "phone",
|
||||
optionValue: location.phone,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeeDefined") {
|
||||
return {
|
||||
value: "somewhereElse",
|
||||
optionValue: location.location,
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
`Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.`
|
||||
);
|
||||
}
|
||||
|
||||
private isBookingLocationWithEventTypeLocations(
|
||||
inputBookingLocation: undefined | string | BookingInputLocation_2024_08_13,
|
||||
dbEventType: EventType
|
||||
) {
|
||||
if (!inputBookingLocation || typeof inputBookingLocation === "string") {
|
||||
// note(Lauris): for backwards compatibility because we had string locations before so let them pass.
|
||||
return true;
|
||||
}
|
||||
|
||||
const eventTypeLocations = this.outputEventTypesService.transformLocations(dbEventType.locations);
|
||||
const allowedLocationTypes = eventTypeLocations.map((location) => location.type);
|
||||
|
||||
const isAllowed = allowedLocationTypes.includes(inputBookingLocation.type);
|
||||
if (!isAllowed) {
|
||||
throw new BadRequestException(
|
||||
`Booking location with type ${inputBookingLocation.type} not valid for event type with id=${
|
||||
dbEventType.id
|
||||
}. The event type has following location types: ${allowedLocationTypes.join(
|
||||
", "
|
||||
)}, and only these types are allowed for booking location.`
|
||||
);
|
||||
}
|
||||
|
||||
if (inputBookingLocation.type === "integration" && "integration" in inputBookingLocation) {
|
||||
const allowedIntegrations = eventTypeLocations
|
||||
.filter((location) => location.type === "integration")
|
||||
.map((location) => location.integration);
|
||||
|
||||
const isAllowedIntegration = allowedIntegrations.includes(inputBookingLocation.integration);
|
||||
if (!isAllowedIntegration) {
|
||||
throw new BadRequestException(
|
||||
`Booking location with integration ${
|
||||
inputBookingLocation.integration
|
||||
} not valid for event type with id=${
|
||||
dbEventType.id
|
||||
}. The event type has following integrations: ${allowedIntegrations.join(
|
||||
", "
|
||||
)}, and only these integrations are allowed for booking location.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async transformInputCreateRecurringBooking(
|
||||
inputBooking: CreateRecurringBookingInput_2024_08_13,
|
||||
platformClientId?: string
|
||||
) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
|
||||
inputBooking.eventTypeId
|
||||
);
|
||||
@@ -261,7 +395,21 @@ export class InputBookingsService_2024_08_13 {
|
||||
inputBooking.attendee.timeZone
|
||||
);
|
||||
|
||||
const guests = inputBooking.guests;
|
||||
const guests =
|
||||
inputBooking.guests && platformClientId
|
||||
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
|
||||
: inputBooking.guests;
|
||||
const attendeeEmail =
|
||||
inputBooking.attendee.email && platformClientId
|
||||
? await this.platformBookingsService.getPlatformAttendeeEmail(
|
||||
inputBooking.attendee.email,
|
||||
platformClientId
|
||||
)
|
||||
: inputBooking.attendee.email;
|
||||
|
||||
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
|
||||
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
|
||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||
|
||||
for (let i = 0; i < repeatsTimes; i++) {
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
@@ -277,14 +425,13 @@ export class InputBookingsService_2024_08_13 {
|
||||
hasHashedBookingLink: false,
|
||||
guests,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: inputBooking.bookingFieldsResponses
|
||||
? {
|
||||
...inputBooking.bookingFieldsResponses,
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email,
|
||||
guests,
|
||||
}
|
||||
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email, guests },
|
||||
responses: {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
name: inputBooking.attendee.name,
|
||||
email: attendeeEmail,
|
||||
guests,
|
||||
location,
|
||||
},
|
||||
schedulingType: eventType.schedulingType,
|
||||
});
|
||||
|
||||
@@ -315,10 +462,29 @@ export class InputBookingsService_2024_08_13 {
|
||||
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
|
||||
: await this.transformInputRescheduleBooking(bookingUid, body);
|
||||
|
||||
const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId);
|
||||
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(
|
||||
bodyTransformed.eventTypeId
|
||||
);
|
||||
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
let userId: number | undefined = undefined;
|
||||
|
||||
if (
|
||||
oAuthClientParams &&
|
||||
request.body.rescheduledBy &&
|
||||
!request.body.rescheduledBy.includes(oAuthClientParams.platformClientId)
|
||||
) {
|
||||
request.body.rescheduledBy = OAuthClientUsersService.getOAuthUserEmail(
|
||||
oAuthClientParams.platformClientId,
|
||||
request.body.rescheduledBy
|
||||
);
|
||||
}
|
||||
|
||||
if (request.body.rescheduledBy) {
|
||||
if (request.body.rescheduledBy !== bodyTransformed.responses.email) {
|
||||
userId = (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
|
||||
}
|
||||
}
|
||||
|
||||
const location = await this.getRescheduleBookingLocation(bookingUid);
|
||||
if (oAuthClientParams) {
|
||||
@@ -446,7 +612,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
return keyData?.userId;
|
||||
} else {
|
||||
// Access Token
|
||||
return this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
return ownerId;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -465,6 +632,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
|
||||
afterUpdatedDate: queryParams.afterUpdatedAt,
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
afterCreatedDate: queryParams.afterCreatedAt,
|
||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -501,7 +670,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
const oAuthClientParams = booking.eventTypeId
|
||||
? await this.getOAuthClientParams(booking.eventTypeId)
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const newRequest = { ...request };
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PlatformOAuthClient } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class PlatformBookingsService {
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
async getPlatformAttendeesEmails(guestsEmails: string[], platformClientId: string) {
|
||||
return Promise.all(
|
||||
guestsEmails.map((guestEmail) => this.getPlatformAttendeeEmail(guestEmail, platformClientId))
|
||||
);
|
||||
}
|
||||
|
||||
async getPlatformAttendeeEmail(attendeeEmail: string, platformClientId: string) {
|
||||
if (!attendeeEmail.includes(platformClientId)) {
|
||||
// note(Lauris): we need to do this when managed user is booking another managed user and the managed user doing the booking entered their email without oAuth client id
|
||||
// or if one of the guests added are managed users without oAuth client id in their email.
|
||||
const oAuthUserEmail = OAuthClientUsersService.getOAuthUserEmail(platformClientId, attendeeEmail);
|
||||
const oAuthUser = await this.usersRepository.findByEmail(oAuthUserEmail);
|
||||
if (oAuthUser) {
|
||||
return oAuthUserEmail;
|
||||
}
|
||||
}
|
||||
return attendeeEmail;
|
||||
}
|
||||
|
||||
async getOAuthClientParams(eventTypeId: number) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||
|
||||
let oAuthClient: PlatformOAuthClient | null = null;
|
||||
if (eventType?.userId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
|
||||
} else if (eventType?.teamId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
|
||||
}
|
||||
|
||||
if (oAuthClient) {
|
||||
return {
|
||||
platformClientId: oAuthClient.id,
|
||||
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
|
||||
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
||||
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
||||
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from "@calcom/platform-constants";
|
||||
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||
import { ICS_CALENDAR } from "@calcom/platform-constants/apps";
|
||||
import { IcsFeedCalendarService } from "@calcom/platform-libraries";
|
||||
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ class Primary {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
domainWideDelegationCredentialId?: string | null;
|
||||
delegationCredentialId?: string | null;
|
||||
}
|
||||
|
||||
export class Calendar {
|
||||
@@ -174,7 +174,7 @@ export class Calendar {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
domainWideDelegationCredentialId?: string | null;
|
||||
delegationCredentialId?: string | null;
|
||||
}
|
||||
|
||||
export class ConnectedCalendar {
|
||||
@@ -190,7 +190,7 @@ export class ConnectedCalendar {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
domainWideDelegationCredentialId?: string | null;
|
||||
delegationCredentialId?: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@@ -208,7 +208,7 @@ export class ConnectedCalendar {
|
||||
class DestinationCalendar {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
id!: number | string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
@@ -238,7 +238,7 @@ class DestinationCalendar {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
domainWideDelegationCredentialId?: string | null;
|
||||
delegationCredentialId?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
@@ -5,7 +5,8 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||
import { symmetricEncrypt, CalendarService, symmetricDecrypt } from "@calcom/platform-libraries";
|
||||
import { symmetricEncrypt, symmetricDecrypt } from "@calcom/platform-libraries";
|
||||
import { CalendarService } from "@calcom/platform-libraries/app-store";
|
||||
import { Credential } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -39,7 +39,7 @@ export class CalendarsService {
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||
) {}
|
||||
|
||||
private buildNonDwdCredentials<TCredential>(credentials: TCredential[]) {
|
||||
private buildNonDelegationCredentials<TCredential>(credentials: TCredential[]) {
|
||||
return credentials
|
||||
.map((credential) => ({
|
||||
...credential,
|
||||
@@ -83,7 +83,7 @@ export class CalendarsService {
|
||||
);
|
||||
try {
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
||||
this.buildNonDwdCredentials(credentials),
|
||||
this.buildNonDelegationCredentials(credentials),
|
||||
dateFrom,
|
||||
dateTo,
|
||||
composedSelectedCalendars
|
||||
|
||||
@@ -6,7 +6,8 @@ import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/comm
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS, ICS_CALENDAR_TYPE, ICS_CALENDAR } from "@calcom/platform-constants";
|
||||
import { symmetricEncrypt, IcsFeedCalendarService } from "@calcom/platform-libraries";
|
||||
import { symmetricEncrypt } from "@calcom/platform-libraries";
|
||||
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
|
||||
|
||||
@Injectable()
|
||||
export class IcsFeedService implements ICSFeedCalendarApp {
|
||||
|
||||
+2
-2
@@ -33,10 +33,10 @@ import {
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
EventTypesByViewer,
|
||||
EventTypesPublic,
|
||||
eventTypeBookingFields,
|
||||
eventTypeLocations,
|
||||
} from "@calcom/platform-libraries";
|
||||
EventTypesPublic,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
import { getEventTypeById } from "@calcom/platform-libraries/event-types";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+14
-3
@@ -1,6 +1,8 @@
|
||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||
import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable";
|
||||
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
EventTypesPublic,
|
||||
getEventTypesPublic,
|
||||
systemBeforeFieldEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
@@ -68,6 +70,7 @@ export class EventTypesService_2024_04_15 {
|
||||
organization: { isOrgAdmin },
|
||||
profile: { id: profileId },
|
||||
metadata: user.metadata,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,7 +137,7 @@ export class EventTypesService_2024_04_15 {
|
||||
!bookingFields.find((field) => field.type === "email") &&
|
||||
!bookingFields.find((field) => field.type === "phone")
|
||||
) {
|
||||
bookingFields.push(systemBeforeFieldEmail);
|
||||
bookingFields.push({ ...systemBeforeFieldEmail, type: BaseField.email, editable: Editable.system });
|
||||
}
|
||||
|
||||
await updateEventType({
|
||||
@@ -167,7 +170,15 @@ export class EventTypesService_2024_04_15 {
|
||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||
const eventTypeSelectedCalendars =
|
||||
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
|
||||
return {
|
||||
...user,
|
||||
locale: user.locale ?? "en",
|
||||
profile: { id: profileId },
|
||||
userLevelSelectedCalendars: selectedCalendars,
|
||||
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
|
||||
};
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number, userId: number) {
|
||||
|
||||
-1
@@ -1845,7 +1845,6 @@ describe("Event types Endpoints", () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
try {
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
||||
} catch (e) {
|
||||
// Event type might have been deleted by the test
|
||||
}
|
||||
|
||||
+11
-2
@@ -29,7 +29,12 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
@@ -45,8 +50,12 @@ import {
|
||||
@DocsTags("Event Types")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-14\``,
|
||||
description: `Must be set to ${VERSION_2024_06_14}`,
|
||||
example: VERSION_2024_06_14,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_14,
|
||||
},
|
||||
})
|
||||
export class EventTypesController_2024_06_14 {
|
||||
constructor(
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
@@ -76,6 +75,22 @@ export class EventTypesRepository_2024_06_14 {
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeByIdIncludeUsersAndTeam(eventTypeId: number) {
|
||||
const eventType = await this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
include: { users: true, team: true },
|
||||
});
|
||||
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...eventType,
|
||||
recurringEvent: eventType.recurringEvent as Prisma.JsonObject | null | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async getEventTypeByIdWithOwnerAndTeam(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
|
||||
+27
-5
@@ -8,9 +8,13 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries";
|
||||
import {
|
||||
createEventType,
|
||||
updateEventType,
|
||||
getEventTypesPublic,
|
||||
EventTypesPublic,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@@ -112,8 +116,11 @@ export class EventTypesService_2024_06_14 {
|
||||
: false;
|
||||
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
const eventTypeSelectedCalendars =
|
||||
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
|
||||
return {
|
||||
id: user.id,
|
||||
locale: user.locale ?? "en",
|
||||
role: user.role,
|
||||
username: user.username,
|
||||
organizationId: user.organizationId,
|
||||
@@ -121,6 +128,9 @@ export class EventTypesService_2024_06_14 {
|
||||
profile: { id: profileId },
|
||||
metadata: user.metadata,
|
||||
selectedCalendars,
|
||||
email: user.email,
|
||||
userLevelSelectedCalendars: selectedCalendars,
|
||||
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -204,7 +214,11 @@ export class EventTypesService_2024_06_14 {
|
||||
return defaultEventTypes;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) {
|
||||
async updateEventType(
|
||||
eventTypeId: number,
|
||||
body: Partial<InputEventTransformed_2024_06_14>,
|
||||
user: UserWithProfile
|
||||
) {
|
||||
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
|
||||
@@ -230,7 +244,7 @@ export class EventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) {
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined | null) {
|
||||
const existingEventType = await this.getUserEventType(userId, eventTypeId);
|
||||
if (!existingEventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
@@ -242,7 +256,15 @@ export class EventTypesService_2024_06_14 {
|
||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||
const eventTypeSelectedCalendars =
|
||||
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
|
||||
return {
|
||||
...user,
|
||||
locale: user.locale ?? "en",
|
||||
profile: { id: profileId },
|
||||
userLevelSelectedCalendars: selectedCalendars,
|
||||
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
|
||||
};
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number, userId: number) {
|
||||
|
||||
+34
-10
@@ -25,13 +25,17 @@ import {
|
||||
transformSeatsApiToInternal,
|
||||
SystemField,
|
||||
CustomField,
|
||||
} from "@calcom/platform-libraries";
|
||||
InternalLocation,
|
||||
InternalLocationSchema,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
DestinationCalendar_2024_06_14,
|
||||
InputEventTransformed_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
|
||||
|
||||
@@ -58,7 +62,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
const transformedBody = this.transformInputCreateEventType(inputEventType);
|
||||
|
||||
await this.validateEventTypeInputs({
|
||||
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
|
||||
seatsPerTimeSlot: transformedBody?.seatsPerTimeSlot || null,
|
||||
locations: transformedBody.locations,
|
||||
requiresConfirmation: transformedBody.requiresConfirmation,
|
||||
eventName: transformedBody.eventName,
|
||||
@@ -272,7 +276,12 @@ export class InputEventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
|
||||
return transformBookerLayoutsApiToInternal(inputBookerLayouts);
|
||||
const layouts = transformBookerLayoutsApiToInternal(inputBookerLayouts);
|
||||
if (!layouts) return undefined;
|
||||
return {
|
||||
defaultLayout: layouts.defaultLayout as unknown as BookerLayouts,
|
||||
enabledLayouts: layouts.enabledLayouts as unknown as BookerLayouts[],
|
||||
};
|
||||
}
|
||||
|
||||
transformInputConfirmationPolicy(
|
||||
@@ -303,23 +312,22 @@ export class InputEventTypesService_2024_06_14 {
|
||||
eventName,
|
||||
}: ValidationContext) {
|
||||
let seatsPerTimeSlotDb: number | null = null;
|
||||
let locationsDb: ReturnType<typeof this.transformInputLocations> = [];
|
||||
let locationsDb: ReturnType<typeof this.transformLocations> = [];
|
||||
let requiresConfirmationDb = false;
|
||||
|
||||
if (eventTypeId != null) {
|
||||
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId);
|
||||
seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null;
|
||||
locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? [];
|
||||
locationsDb = this.transformLocations(eventTypeDb?.locations) ?? [];
|
||||
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
|
||||
}
|
||||
|
||||
const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb;
|
||||
const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0;
|
||||
const seatsPerTimeSlotFinal = !!seatsPerTimeSlot ? seatsPerTimeSlot : seatsPerTimeSlotDb;
|
||||
const seatsEnabledFinal = !!seatsPerTimeSlotFinal && seatsPerTimeSlotFinal > 0;
|
||||
|
||||
const locationsFinal = locations !== undefined ? locations : locationsDb;
|
||||
const requiresConfirmationFinal =
|
||||
requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb;
|
||||
|
||||
this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal);
|
||||
this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal);
|
||||
this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal);
|
||||
@@ -331,7 +339,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
}
|
||||
validateSeatsSingleLocationRule(
|
||||
seatsEnabled: boolean,
|
||||
locations: ReturnType<typeof this.transformInputLocations>
|
||||
locations: ReturnType<typeof this.transformLocations>
|
||||
) {
|
||||
if (seatsEnabled && locations.length > 1) {
|
||||
throw new BadRequestException(
|
||||
@@ -339,6 +347,22 @@ export class InputEventTypesService_2024_06_14 {
|
||||
);
|
||||
}
|
||||
}
|
||||
transformLocations(locations: any) {
|
||||
if (!locations) return [];
|
||||
|
||||
const knownLocations: InternalLocation[] = [];
|
||||
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
|
||||
|
||||
for (const location of locations) {
|
||||
const result = InternalLocationSchema.safeParse(location);
|
||||
if (result.success) {
|
||||
knownLocations.push(result.data);
|
||||
} else {
|
||||
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
|
||||
}
|
||||
}
|
||||
return [...knownLocations];
|
||||
}
|
||||
|
||||
validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) {
|
||||
if (seatsEnabled && requiresConfirmation) {
|
||||
@@ -349,7 +373,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
validateMultipleLocationsSeatsDisabledRule(
|
||||
locations: ReturnType<typeof this.transformInputLocations>,
|
||||
locations: ReturnType<typeof this.transformLocations>,
|
||||
seatsEnabled: boolean
|
||||
) {
|
||||
if (locations.length > 1 && seatsEnabled) {
|
||||
|
||||
+11
-8
@@ -2,15 +2,18 @@ import { Injectable } from "@nestjs/common";
|
||||
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
|
||||
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
userMetadata,
|
||||
parseBookingLimit,
|
||||
parseRecurringEvent,
|
||||
getBookingFieldsWithSystemFields,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
transformLocationsInternalToApi,
|
||||
transformBookingFieldsInternalToApi,
|
||||
parseRecurringEvent,
|
||||
InternalLocationSchema,
|
||||
SystemField,
|
||||
CustomField,
|
||||
parseBookingLimit,
|
||||
transformIntervalLimitsInternalToApi,
|
||||
transformFutureBookingLimitsInternalToApi,
|
||||
transformRecurrenceInternalToApi,
|
||||
@@ -21,8 +24,7 @@ import {
|
||||
transformSeatsInternalToApi,
|
||||
InternalLocation,
|
||||
BookingFieldSchema,
|
||||
getBookingFieldsWithSystemFields,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
BookerLayoutsTransformedSchema,
|
||||
@@ -198,13 +200,13 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
transformLocations(locations: any) {
|
||||
if (!locations) return [];
|
||||
transformLocations(locationDb: any) {
|
||||
if (!locationDb) return [];
|
||||
|
||||
const knownLocations: InternalLocation[] = [];
|
||||
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
|
||||
|
||||
for (const location of locations) {
|
||||
for (const location of locationDb) {
|
||||
const result = InternalLocationSchema.safeParse(location);
|
||||
if (result.success) {
|
||||
knownLocations.push(result.data);
|
||||
@@ -315,6 +317,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
transformEventTypeColor(eventTypeColor: any) {
|
||||
if (!eventTypeColor) return undefined;
|
||||
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
|
||||
if (!parsedeventTypeColor) return undefined;
|
||||
return transformEventTypeColorsInternalToApi(parsedeventTypeColor);
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -39,8 +39,12 @@ import {
|
||||
@DocsTags("Schedules")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-11\``,
|
||||
description: `Must be set to ${VERSION_2024_06_11}`,
|
||||
example: VERSION_2024_06_11,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_11,
|
||||
},
|
||||
})
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries";
|
||||
import {
|
||||
transformApiScheduleOverrides,
|
||||
transformApiScheduleAvailability,
|
||||
} from "@calcom/platform-libraries/schedules";
|
||||
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CalendarAppError,
|
||||
CalendarAppDelegationCredentialInvalidGrantError,
|
||||
CalendarAppDelegationCredentialError,
|
||||
CalendarAppDelegationCredentialConfigurationError,
|
||||
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
|
||||
CalendarAppDelegationCredentialNotSetupError,
|
||||
} from "@calcom/platform-libraries/app-store";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
type CalendarError =
|
||||
| CalendarAppError
|
||||
| CalendarAppDelegationCredentialInvalidGrantError
|
||||
| CalendarAppDelegationCredentialError
|
||||
| CalendarAppDelegationCredentialConfigurationError
|
||||
| CalendarAppDelegationCredentialNotSetupError
|
||||
| CalendarAppDelegationCredentialClientIdNotAuthorizedError;
|
||||
@Catch(
|
||||
CalendarAppError,
|
||||
CalendarAppDelegationCredentialInvalidGrantError,
|
||||
CalendarAppDelegationCredentialError,
|
||||
CalendarAppDelegationCredentialConfigurationError,
|
||||
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
|
||||
CalendarAppDelegationCredentialNotSetupError
|
||||
)
|
||||
export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger("CalendarServiceExceptionFilter");
|
||||
|
||||
catch(exception: CalendarError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: filterReqHeaders(request.headers),
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
|
||||
response.status(400).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
error: { code: exception.name, message: exception.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -18,7 +19,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
headers: filterReqHeaders(request.headers),
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||
import {
|
||||
@@ -44,7 +45,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
this.logger.error(`PrismaError: ${error.message}`, {
|
||||
error,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
headers: filterReqHeaders(request.headers),
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -46,7 +47,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
headers: filterReqHeaders(request.headers),
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
@@ -19,7 +20,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
||||
this.logger.error(`ZodError: ${error.message}`, {
|
||||
error,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
headers: filterReqHeaders(request.headers),
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IncomingHttpHeaders } from "http";
|
||||
|
||||
export function filterReqHeaders(headers: IncomingHttpHeaders): Partial<IncomingHttpHeaders> {
|
||||
return {
|
||||
"Content-Type": headers["Content-Type"] ?? headers["content-type"],
|
||||
Authorization: headers["Authorization"] ?? headers["authorization"],
|
||||
"X-Cal-Client-Id": headers["X-Cal-Client-Id"] ?? headers["x-cal-client-id"],
|
||||
"Cal-Api-Version": headers["Cal-Api-Version"] ?? headers["cal-api-version"],
|
||||
"X-Request-Id": headers["X-Request-Id"] ?? headers["x-request-id"],
|
||||
"User-Agent": headers["User-agent"] ?? headers["user-agent"],
|
||||
"X-Forwarded-For": headers["x-forwarded-for"],
|
||||
"X-Forwarded-Host": headers["x-forwarded-host"],
|
||||
"CF-Connecting-IP": headers["cf-connecting-ip"],
|
||||
"CloudFront-Viewer-Address": headers["cloudfront-viewer-address"],
|
||||
"X-Vercel-Id": headers["x-vercel-id"],
|
||||
"X-Vercel-Deployment-Url": headers["x-vercel-deployment-url"],
|
||||
"X-Vercel-Country": headers["x-vercel-country"],
|
||||
"X-Vercel-Region": headers["x-vercel-region"],
|
||||
Host: headers["Host"] ?? headers["host"],
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { v4 as uuid } from "uuid";
|
||||
@@ -15,7 +16,7 @@ export class RequestIdMiddleware implements NestMiddleware {
|
||||
requestId,
|
||||
method,
|
||||
url: baseUrl,
|
||||
headers,
|
||||
headers: filterReqHeaders(headers),
|
||||
requestBody,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries";
|
||||
import { ConnectedApps } from "@calcom/platform-libraries";
|
||||
import { ConnectedApps } from "@calcom/platform-libraries/app-store";
|
||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
/*
|
||||
@@ -81,7 +81,7 @@ export class AtomsController {
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const { teamId } = queryParams;
|
||||
|
||||
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user.id, user.name, teamId);
|
||||
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -124,7 +124,11 @@ export class AtomsController {
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Body() body: UpdateEventTypeReturn
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||
const eventType = await this.eventTypesService.updateEventType(
|
||||
eventTypeId,
|
||||
{ ...body, id: eventTypeId },
|
||||
user
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
@@ -140,7 +144,12 @@ export class AtomsController {
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Body() body: UpdateEventTypeReturn
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
||||
const eventType = await this.eventTypesService.updateTeamEventType(eventTypeId, body, user, teamId);
|
||||
const eventType = await this.eventTypesService.updateTeamEventType(
|
||||
eventTypeId,
|
||||
{ ...body, id: eventTypeId },
|
||||
user,
|
||||
teamId
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getConnectedApps, ConnectedApps } from "@calcom/platform-libraries";
|
||||
import { getConnectedApps, ConnectedApps } from "@calcom/platform-libraries/app-store";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -9,28 +9,29 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
|
||||
import { MembershipRole, getClientSecretFromPayment } from "@calcom/platform-libraries";
|
||||
import type { TeamQuery } from "@calcom/platform-libraries";
|
||||
import { enrichUserWithDelegationConferencingCredentialsWithoutOrgId } from "@calcom/platform-libraries/app-store";
|
||||
import { getEnabledAppsFromCredentials, getAppFromSlug } from "@calcom/platform-libraries/app-store";
|
||||
import type {
|
||||
App,
|
||||
TDependencyData,
|
||||
CredentialOwner,
|
||||
CredentialPayload,
|
||||
CredentialDataWithTeamName,
|
||||
LocationOption,
|
||||
} from "@calcom/platform-libraries/app-store";
|
||||
import {
|
||||
getBulkEventTypes,
|
||||
getEventTypeById,
|
||||
bulkUpdateEventsToDefaultLocation,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
updateEventType,
|
||||
TUpdateEventTypeInputSchema,
|
||||
systemBeforeFieldEmail,
|
||||
getEventTypeById,
|
||||
getEnabledAppsFromCredentials,
|
||||
getAppFromSlug,
|
||||
MembershipRole,
|
||||
EventTypeMetaDataSchema,
|
||||
getClientSecretFromPayment,
|
||||
getBulkEventTypes,
|
||||
bulkUpdateEventsToDefaultLocation,
|
||||
} from "@calcom/platform-libraries";
|
||||
import type {
|
||||
App,
|
||||
CredentialDataWithTeamName,
|
||||
LocationOption,
|
||||
TeamQuery,
|
||||
CredentialOwner,
|
||||
TDependencyData,
|
||||
CredentialPayload,
|
||||
} from "@calcom/platform-libraries";
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type EnabledAppType = App & {
|
||||
@@ -93,9 +94,10 @@ export class EventTypesAtomService {
|
||||
) {
|
||||
await this.checkCanUpdateTeamEventType(user.id, eventTypeId, teamId, body.scheduleId);
|
||||
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
|
||||
const bookingFields = [...(body.bookingFields || [])];
|
||||
const bookingFields = body.bookingFields ? [...body.bookingFields] : undefined;
|
||||
|
||||
if (
|
||||
bookingFields?.length &&
|
||||
!bookingFields.find((field) => field.type === "email") &&
|
||||
!bookingFields.find((field) => field.type === "phone")
|
||||
) {
|
||||
@@ -103,7 +105,7 @@ export class EventTypesAtomService {
|
||||
}
|
||||
|
||||
const eventType = await updateEventType({
|
||||
input: { id: eventTypeId, ...body, bookingFields },
|
||||
input: { ...body, id: eventTypeId, bookingFields },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -116,7 +118,7 @@ export class EventTypesAtomService {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return eventType.eventType;
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
|
||||
@@ -133,7 +135,7 @@ export class EventTypesAtomService {
|
||||
}
|
||||
|
||||
const eventType = await updateEventType({
|
||||
input: { id: eventTypeId, ...body, bookingFields },
|
||||
input: { ...body, id: eventTypeId, bookingFields },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -146,10 +148,15 @@ export class EventTypesAtomService {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return eventType.eventType;
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async checkCanUpdateTeamEventType(userId: number, eventTypeId: number, teamId: number, scheduleId: number) {
|
||||
async checkCanUpdateTeamEventType(
|
||||
userId: number,
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
scheduleId: number | null | undefined
|
||||
) {
|
||||
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
|
||||
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
|
||||
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
|
||||
@@ -178,11 +185,11 @@ export class EventTypesAtomService {
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTypesAppIntegration(slug: string, userId: number, userName: string | null, teamId?: number) {
|
||||
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
|
||||
async getEventTypesAppIntegration(slug: string, user: UserWithProfile, teamId?: number) {
|
||||
let credentials = await this.credentialsRepository.getAllUserCredentialsById(user.id);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
if (teamId) {
|
||||
const teamsQuery = await this.atomsRepository.getUserTeams(userId);
|
||||
const teamsQuery = await this.atomsRepository.getUserTeams(user.id);
|
||||
// If a team is a part of an org then include those apps
|
||||
// Don't want to iterate over these parent teams
|
||||
const filteredTeams: TeamQuery[] = [];
|
||||
@@ -210,8 +217,17 @@ export class EventTypesAtomService {
|
||||
credentials = credentials.concat(teamAppCredentials);
|
||||
}
|
||||
}
|
||||
//TODO: enrich credentials for DWD
|
||||
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
|
||||
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
|
||||
const { credentials: allCredentials } = await enrichUserWithDelegationConferencingCredentialsWithoutOrgId(
|
||||
{
|
||||
user: {
|
||||
...user,
|
||||
credentials,
|
||||
},
|
||||
}
|
||||
);
|
||||
credentials = allCredentials;
|
||||
const enabledApps = await getEnabledAppsFromCredentials(allCredentials, {
|
||||
where: { slug },
|
||||
});
|
||||
const apps = await Promise.all(
|
||||
@@ -264,7 +280,7 @@ export class EventTypesAtomService {
|
||||
});
|
||||
}
|
||||
const credentialOwner: CredentialOwner = {
|
||||
name: userName,
|
||||
name: user.name,
|
||||
teamId,
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
Redirect,
|
||||
UnauthorizedException,
|
||||
Req,
|
||||
HttpException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
@@ -56,7 +57,7 @@ export type OAuthCallbackState = {
|
||||
})
|
||||
@DocsTags("Conferencing")
|
||||
export class ConferencingController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
private readonly logger = new Logger("ConferencingController");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
@@ -144,6 +145,10 @@ export class ConferencingController {
|
||||
@Query("error") error: string | undefined,
|
||||
@Query("error_description") error_description: string | undefined
|
||||
): Promise<{ url: string }> {
|
||||
if (!state) {
|
||||
throw new BadRequestException("Missing `state` query param");
|
||||
}
|
||||
|
||||
const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
|
||||
try {
|
||||
const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
|
||||
@@ -169,6 +174,9 @@ export class ConferencingController {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException || error instanceof Error) {
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
return {
|
||||
url: decodedCallbackState.onErrorReturnTo ?? "",
|
||||
};
|
||||
|
||||
@@ -5,12 +5,8 @@ import { BadRequestException, InternalServerErrorException, Logger } from "@nest
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { CONFERENCING_APPS, CAL_VIDEO } from "@calcom/platform-constants";
|
||||
import {
|
||||
userMetadata,
|
||||
handleDeleteCredential,
|
||||
getApps,
|
||||
getUsersCredentials,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { userMetadata, getUsersCredentials } from "@calcom/platform-libraries";
|
||||
import { getApps, handleDeleteCredential } from "@calcom/platform-libraries/app-store";
|
||||
|
||||
@Injectable()
|
||||
export class ConferencingService {
|
||||
|
||||
+2
-2
@@ -29,12 +29,12 @@ export class DestinationCalendarsController {
|
||||
@Body() input: DestinationCalendarsInputBodyDto,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<DestinationCalendarsOutputResponseDto> {
|
||||
const { integration, externalId, domainWideDelegationCredentialId } = input;
|
||||
const { integration, externalId, delegationCredentialId } = input;
|
||||
const updatedDestinationCalendar = await this.destinationCalendarsService.updateDestinationCalendars(
|
||||
integration,
|
||||
externalId,
|
||||
user.id,
|
||||
domainWideDelegationCredentialId
|
||||
delegationCredentialId
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -12,21 +12,21 @@ export class DestinationCalendarsRepository {
|
||||
userId: number,
|
||||
primaryEmail: string | null,
|
||||
credentialId?: number,
|
||||
domainWideDelegationCredentialId?: string
|
||||
delegationCredentialId?: string
|
||||
) {
|
||||
return await this.dbWrite.prisma.destinationCalendar.upsert({
|
||||
update: {
|
||||
integration,
|
||||
externalId,
|
||||
...(credentialId ? { credentialId } : {}),
|
||||
...(domainWideDelegationCredentialId ? { domainWideDelegationCredentialId } : {}),
|
||||
...(delegationCredentialId ? { delegationCredentialId } : {}),
|
||||
primaryEmail,
|
||||
},
|
||||
create: {
|
||||
integration,
|
||||
externalId,
|
||||
...(credentialId ? { credentialId } : {}),
|
||||
...(domainWideDelegationCredentialId ? { domainWideDelegationCredentialId } : {}),
|
||||
...(delegationCredentialId ? { delegationCredentialId } : {}),
|
||||
primaryEmail,
|
||||
userId,
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ export class DestinationCalendarsInputBodyDto {
|
||||
@ApiProperty({
|
||||
example: "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
|
||||
description:
|
||||
"Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint",
|
||||
"Unique identifier used to represent the specific calendar, as returned by the /calendars endpoint",
|
||||
type: "string",
|
||||
required: true,
|
||||
})
|
||||
@@ -37,5 +37,5 @@ export class DestinationCalendarsInputBodyDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readonly domainWideDelegationCredentialId?: string;
|
||||
readonly delegationCredentialId?: string;
|
||||
}
|
||||
|
||||
+11
-11
@@ -14,10 +14,10 @@ export class DestinationCalendarsService {
|
||||
integration: string,
|
||||
externalId: string,
|
||||
userId: number,
|
||||
domainWideDelegationCredentialId?: string
|
||||
delegationCredentialId?: string
|
||||
) {
|
||||
// note(Lauris): todo remove the log but leaving this now to confirm domainWideDelegationCredentialId is received
|
||||
console.log("debug: domainWideDelegationCredentialId", domainWideDelegationCredentialId);
|
||||
// note(Lauris): todo remove the log but leaving this now to confirm delegationCredentialId is received
|
||||
console.log("debug: delegationCredentialId", delegationCredentialId);
|
||||
const userCalendars = await this.calendarsService.getCalendars(userId);
|
||||
const allCalendars: Calendar[] = userCalendars.connectedCalendars
|
||||
.map((cal: ConnectedCalendar) => cal.calendars ?? [])
|
||||
@@ -27,25 +27,25 @@ export class DestinationCalendarsService {
|
||||
cal.externalId === externalId && cal.integration === integration && cal.readOnly === false
|
||||
)?.credentialId;
|
||||
|
||||
if (!domainWideDelegationCredentialId && !credentialId) {
|
||||
if (!delegationCredentialId && !credentialId) {
|
||||
throw new NotFoundException(`Could not find calendar ${externalId}`);
|
||||
}
|
||||
|
||||
const dwdCalendar = domainWideDelegationCredentialId
|
||||
const delegatedCalendar = delegationCredentialId
|
||||
? allCalendars.find(
|
||||
(cal: Calendar) =>
|
||||
cal.externalId === externalId &&
|
||||
cal.integration === integration &&
|
||||
cal.domainWideDelegationCredentialId === domainWideDelegationCredentialId
|
||||
cal.delegationCredentialId === delegationCredentialId
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (domainWideDelegationCredentialId && !dwdCalendar) {
|
||||
if (delegationCredentialId && !delegatedCalendar) {
|
||||
throw new NotFoundException(`Could not find calendar ${externalId}`);
|
||||
}
|
||||
|
||||
const primaryEmail = dwdCalendar
|
||||
? (dwdCalendar.primary && dwdCalendar?.email) || undefined
|
||||
const primaryEmail = delegatedCalendar
|
||||
? (delegatedCalendar.primary && delegatedCalendar?.email) || undefined
|
||||
: allCalendars.find((cal: Calendar) => cal.primary && cal.credentialId === credentialId)?.email;
|
||||
|
||||
const {
|
||||
@@ -58,8 +58,8 @@ export class DestinationCalendarsService {
|
||||
externalId,
|
||||
userId,
|
||||
primaryEmail ?? null,
|
||||
dwdCalendar ? undefined : credentialId,
|
||||
dwdCalendar ? domainWideDelegationCredentialId : undefined
|
||||
delegatedCalendar ? undefined : credentialId,
|
||||
delegatedCalendar ? delegationCredentialId : undefined
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries";
|
||||
import { getTranslation } from "@calcom/platform-libraries";
|
||||
import { sendSignupToOrganizationEmail } from "@calcom/platform-libraries/emails";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
|
||||
+2
-2
@@ -282,13 +282,13 @@ describe("EventTypes WebhooksController (e2e)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${eventType.id}/webhooks/1234453`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to user", () => {
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.expect(403);
|
||||
|
||||
@@ -23,7 +23,7 @@ export class JwtService {
|
||||
}
|
||||
|
||||
getIssuedAtTime() {
|
||||
// divided by 1000 because iat (issued at time) is in seconds (not milliseconds) as informed by JWT speficication
|
||||
// divided by 1000 because iat (issued at time) is in seconds (not milliseconds) as informed by JWT specification
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { ArrayNotEmpty, isEmail, IsOptional } from "class-validator";
|
||||
|
||||
import { Pagination } from "@calcom/platform-types";
|
||||
|
||||
export class GetManagedUsersInput extends Pagination {
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
return value.split(",").map((email: string) => {
|
||||
const trimmed = email.trim();
|
||||
if (!isEmail(trimmed)) {
|
||||
throw new BadRequestException(`Invalid email ${trimmed}`);
|
||||
}
|
||||
return trimmed;
|
||||
});
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@ArrayNotEmpty({ message: "emails cannot be empty." })
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Filter managed users by email. If you want to filter by multiple emails, separate them with a comma.",
|
||||
example: "?emails=email1@example.com,email2@example.com",
|
||||
})
|
||||
emails?: string[];
|
||||
}
|
||||
+172
-17
@@ -7,6 +7,7 @@ import { Locales } from "@/lib/enums/locales";
|
||||
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -25,6 +26,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { slugify } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:4321";
|
||||
@@ -90,8 +92,11 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
const platformAdminEmail = `oauth-client-users-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
|
||||
const userEmail = `oauth-client-users-user-${randomString()}@api.com`;
|
||||
const userEmail = `oauth-client-users-user-${randomString(5)}@api.com`;
|
||||
const userTimeZone = "Europe/Rome";
|
||||
const userEmailTwo = `oauth-client-users-user-2-${randomString(5)}@api.com`;
|
||||
const userTimeZoneTwo = "Europe/Rome";
|
||||
let postResponseDataTwo: CreateManagedUserOutput["data"];
|
||||
|
||||
const userEmail2 = `oauth-client-users-user2-${randomString()}@api.com`;
|
||||
const userTimeZone2 = "America/New_York";
|
||||
@@ -215,7 +220,53 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart);
|
||||
expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat);
|
||||
expect(responseBody.data.user.locale).toEqual(requestBody.locale);
|
||||
expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl);
|
||||
const [emailUser, emailDomain] = responseBody.data.user.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.user.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
await userConnectedToOAuth(oAuthClient.id, responseBody.data.user.email, 1);
|
||||
await userHasDefaultEventTypes(responseBody.data.user.id);
|
||||
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
|
||||
await userHasOnlyOneSchedule(responseBody.data.user.id);
|
||||
});
|
||||
|
||||
it(`/POST with default event types`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: userEmailTwo,
|
||||
timeZone: userTimeZoneTwo,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Bob Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
|
||||
postResponseDataTwo = responseBody.data;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart);
|
||||
@@ -225,7 +276,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
await userConnectedToOAuth(oAuthClient.id, responseBody.data.user.email);
|
||||
await userConnectedToOAuth(oAuthClient.id, responseBody.data.user.email, 2);
|
||||
await userHasDefaultEventTypes(responseBody.data.user.id);
|
||||
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
|
||||
await userHasOnlyOneSchedule(responseBody.data.user.id);
|
||||
@@ -255,7 +306,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
getOAuthUserEmail(oAuthClientEventTypesDisabled.id, requestBody.email)
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClientEventTypesDisabled.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
@@ -266,17 +317,17 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
await userConnectedToOAuth(oAuthClientEventTypesDisabled.id, responseBody.data.user.email);
|
||||
await userConnectedToOAuth(oAuthClientEventTypesDisabled.id, responseBody.data.user.email, 1);
|
||||
await userDoesNotHaveDefaultEventTypes(responseBody.data.user.id);
|
||||
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
|
||||
await userHasOnlyOneSchedule(responseBody.data.user.id);
|
||||
});
|
||||
|
||||
async function userConnectedToOAuth(oAuthClientId: string, userEmail: string) {
|
||||
async function userConnectedToOAuth(oAuthClientId: string, userEmail: string, usersCount: number) {
|
||||
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId);
|
||||
const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail);
|
||||
|
||||
expect(oAuthUsers?.length).toEqual(1);
|
||||
expect(oAuthUsers?.length).toEqual(usersCount);
|
||||
expect(newOAuthUser?.email).toEqual(userEmail);
|
||||
}
|
||||
|
||||
@@ -359,6 +410,110 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data?.length).toBeGreaterThan(0);
|
||||
expect(responseBody.data[0].email).toEqual(postResponseData.user.email);
|
||||
expect(responseBody.data[0].name).toEqual(postResponseData.user.name);
|
||||
expect(responseBody.data?.length).toEqual(2);
|
||||
const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email);
|
||||
const userTwo = responseBody.data.find((user) => user.email === postResponseDataTwo.user.email);
|
||||
expect(userOne?.email).toEqual(postResponseData.user.email);
|
||||
expect(userOne?.name).toEqual(postResponseData.user.name);
|
||||
expect(userTwo?.email).toEqual(postResponseDataTwo.user.email);
|
||||
expect(userTwo?.name).toEqual(postResponseDataTwo.user.name);
|
||||
});
|
||||
|
||||
it(`/GET: managed user by original email`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${userEmail}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetManagedUsersOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email);
|
||||
expect(userOne?.email).toEqual(postResponseData.user.email);
|
||||
expect(userOne?.name).toEqual(postResponseData.user.name);
|
||||
});
|
||||
|
||||
it(`/GET: managed users by original emails`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${userEmail},${userEmailTwo}`
|
||||
)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetManagedUsersOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(2);
|
||||
const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email);
|
||||
const userTwo = responseBody.data.find((user) => user.email === postResponseDataTwo.user.email);
|
||||
expect(userOne?.email).toEqual(postResponseData.user.email);
|
||||
expect(userOne?.name).toEqual(postResponseData.user.name);
|
||||
expect(userTwo?.email).toEqual(postResponseDataTwo.user.email);
|
||||
expect(userTwo?.name).toEqual(postResponseDataTwo.user.name);
|
||||
});
|
||||
|
||||
it(`/GET: managed user by oAuth email`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
// note(Lauris): we use encodeURIComponent because email stored on our side includes "+" which without encoding becomes an empty space.
|
||||
.get(
|
||||
`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${encodeURIComponent(
|
||||
postResponseData.user.email
|
||||
)}`
|
||||
)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetManagedUsersOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email);
|
||||
expect(userOne?.email).toEqual(postResponseData.user.email);
|
||||
expect(userOne?.name).toEqual(postResponseData.user.name);
|
||||
});
|
||||
|
||||
it(`should error /GET if managed user email is invalid`, async () => {
|
||||
const invalidEmail = "invalid-email";
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${invalidEmail}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body?.error?.message).toEqual(`Invalid email ${invalidEmail}`);
|
||||
});
|
||||
|
||||
it(`/GET: managed users by oAuth emails`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
// note(Lauris): we use encodeURIComponent because email stored on our side includes "+" which without encoding becomes an empty space.
|
||||
.get(
|
||||
`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${encodeURIComponent(
|
||||
postResponseData.user.email
|
||||
)},${encodeURIComponent(postResponseDataTwo.user.email)}`
|
||||
)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetManagedUsersOutput = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(2);
|
||||
const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email);
|
||||
const userTwo = responseBody.data.find((user) => user.email === postResponseDataTwo.user.email);
|
||||
expect(userOne?.email).toEqual(postResponseData.user.email);
|
||||
expect(userOne?.name).toEqual(postResponseData.user.name);
|
||||
expect(userTwo?.email).toEqual(postResponseDataTwo.user.email);
|
||||
expect(userTwo?.name).toEqual(postResponseDataTwo.user.name);
|
||||
});
|
||||
|
||||
it(`/GET/:id`, async () => {
|
||||
@@ -372,7 +527,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.email).toEqual(getOAuthUserEmail(oAuthClient.id, userEmail));
|
||||
expect(responseBody.data.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userEmail)
|
||||
);
|
||||
});
|
||||
|
||||
it(`/PUT/:id`, async () => {
|
||||
@@ -390,7 +547,12 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.email).toEqual(getOAuthUserEmail(oAuthClient.id, userUpdatedEmail));
|
||||
expect(responseBody.data.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userUpdatedEmail)
|
||||
);
|
||||
const [emailUser, emailDomain] = responseBody.data.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.locale).toEqual(Locales.PT_BR);
|
||||
});
|
||||
|
||||
@@ -402,13 +564,6 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
|
||||
const [username, emailDomain] = userEmail.split("@");
|
||||
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClientEventTypesDisabled.id);
|
||||
@@ -452,7 +607,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let postResponseData: CreateManagedUserOutput["data"];
|
||||
|
||||
const userEmail = `oauth-client-users-user-${randomString()}@api.com`;
|
||||
const userEmail = `oauth-client-users-user-${randomString(5)}@api.com`;
|
||||
const userTimeZone = "Europe/Rome";
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
+4
-9
@@ -3,6 +3,7 @@ import { Locales } from "@/lib/enums/locales";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input";
|
||||
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||
@@ -57,20 +58,14 @@ export class OAuthClientUsersController {
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async getManagedUsers(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Query() queryParams: Pagination
|
||||
@Query() queryParams: GetManagedUsersInput
|
||||
): Promise<GetManagedUsersOutput> {
|
||||
this.logger.log(`getting managed users with data for OAuth Client with ID ${oAuthClientId}`);
|
||||
const { offset, limit } = queryParams;
|
||||
|
||||
const existingUsers = await this.userRepository.findManagedUsersByOAuthClientId(
|
||||
oAuthClientId,
|
||||
offset ?? 0,
|
||||
limit ?? 50
|
||||
);
|
||||
const managedUsers = await this.oAuthClientUsersService.getManagedUsers(oAuthClientId, queryParams);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: existingUsers.map((user) => this.getResponseUser(user)),
|
||||
data: managedUsers.map((user) => this.getResponseUser(user)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { IsEnum, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class CreateManagedUserData {
|
||||
export class CreateManagedUserData {
|
||||
@ApiProperty({
|
||||
type: ManagedUserOutput,
|
||||
})
|
||||
|
||||
+1
-1
@@ -274,7 +274,7 @@ describe("OAuth client WebhooksController (e2e)", () => {
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/oauth-clients/${oAuthClient.id}/webhooks/1234453`)
|
||||
.expect(404);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
@@ -36,7 +36,7 @@ export class OAuthClientUsersService {
|
||||
"You cannot create a managed user outside of an organization - the OAuth client does not belong to any organization."
|
||||
);
|
||||
} else {
|
||||
const email = this.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
const email = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
user = (
|
||||
await createNewUsersConnectToOrgIfExists({
|
||||
invitations: [
|
||||
@@ -98,22 +98,41 @@ export class OAuthClientUsersService {
|
||||
}
|
||||
|
||||
async getExistingUserByEmail(oAuthClientId: string, email: string) {
|
||||
const oAuthEmail = this.getOAuthUserEmail(oAuthClientId, email);
|
||||
const oAuthEmail = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, email);
|
||||
return await this.userRepository.findByEmail(oAuthEmail);
|
||||
}
|
||||
|
||||
async getManagedUsers(oAuthClientId: string, queryParams: GetManagedUsersInput) {
|
||||
const { offset, limit, emails } = queryParams;
|
||||
|
||||
const oAuthEmails = emails?.map((email) =>
|
||||
email.includes(oAuthClientId) ? email : OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, email)
|
||||
);
|
||||
|
||||
const managedUsers = await this.userRepository.findManagedUsersByOAuthClientIdAndEmails(
|
||||
oAuthClientId,
|
||||
offset ?? 0,
|
||||
limit ?? 50,
|
||||
oAuthEmails
|
||||
);
|
||||
|
||||
return managedUsers;
|
||||
}
|
||||
|
||||
async updateOAuthClientUser(oAuthClientId: string, userId: number, body: UpdateManagedUserInput) {
|
||||
if (body.email) {
|
||||
const emailWithOAuthId = this.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
const emailWithOAuthId = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
body.email = emailWithOAuthId;
|
||||
const newUsername = slugify(emailWithOAuthId);
|
||||
const [emailUser, emailDomain] = emailWithOAuthId.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
const newUsername = slugify(`${emailUser}-${domainName}-${TLD}`);
|
||||
await this.userRepository.updateUsername(userId, newUsername);
|
||||
}
|
||||
|
||||
return this.userRepository.update(userId, body);
|
||||
}
|
||||
|
||||
getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
|
||||
static getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
|
||||
const [username, emailDomain] = userEmail.split("@");
|
||||
return `${username}+${oAuthClientId}@${emailDomain}`;
|
||||
}
|
||||
|
||||
+5
-3
@@ -2,12 +2,12 @@ import {
|
||||
GoogleServiceAccountKeyInput,
|
||||
ServiceAccountKeyValidator,
|
||||
MicrosoftServiceAccountKeyInput,
|
||||
} from "@/modules/organizations/dwd/inputs/service-account-key.input";
|
||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, IsNotEmpty, ValidateNested, Validate } from "class-validator";
|
||||
|
||||
export class CreateDwdInput {
|
||||
export class CreateDelegationCredentialInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty()
|
||||
@@ -30,5 +30,7 @@ export class CreateDwdInput {
|
||||
})
|
||||
@Expose()
|
||||
@Type(() => Object)
|
||||
serviceAccountKey!: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput;
|
||||
serviceAccountKey!: (GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput) & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user