Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b84405c5eb | ||
|
|
2743b32536 | ||
|
|
a251422f7a | ||
|
|
d7c9bd6ed8 | ||
|
|
8b3ff0b789 | ||
|
|
94a537bfda | ||
|
|
64305ffee1 | ||
|
|
45698db0b4 | ||
|
|
14e14289f0 | ||
|
|
18650b1215 | ||
|
|
d2bbbf3f45 | ||
|
|
13cb99b7fe | ||
|
|
a1571d01bd | ||
|
|
bbd8af074e | ||
|
|
c9a47dd0ce | ||
|
|
1c9778e667 | ||
|
|
758143ef70 | ||
|
|
b5911a9a2d | ||
|
|
260f4e1b51 | ||
|
|
e6c36af3b4 | ||
|
|
2fb2458eee | ||
|
|
b340b0edbe | ||
|
|
f7fd1c4402 | ||
|
|
c316721051 | ||
|
|
80ac118242 | ||
|
|
efd15c5a3e | ||
|
|
7d02d1bb23 | ||
|
|
0d877e7be3 | ||
|
|
e4c47640fc | ||
|
|
89e7fca183 | ||
|
|
33b5bf1db8 | ||
|
|
f69c4a900e | ||
|
|
1da25cc85f | ||
|
|
3a65b372c5 | ||
|
|
9892d521fb | ||
|
|
3285b252c2 | ||
|
|
97af2fc0f9 | ||
|
|
5c508bce6d | ||
|
|
db84343581 | ||
|
|
2b44d09191 | ||
|
|
10d67aee86 | ||
|
|
b33bbb8af0 | ||
|
|
3fe9327724 | ||
|
|
179a547bd0 | ||
|
|
7ada02177d | ||
|
|
59b55e40b5 | ||
|
|
19563aa697 | ||
|
|
6410687fb6 | ||
|
|
4e4c8f5f49 | ||
|
|
416b17cf69 | ||
|
|
27cd7c8496 | ||
|
|
02321c33b2 | ||
|
|
c3dab3e4f8 | ||
|
|
0c182e2813 | ||
|
|
51a076212b | ||
|
|
27dce7374b | ||
|
|
52f2c18800 | ||
|
|
35a8918ea9 | ||
|
|
b39eab72e7 | ||
|
|
928b2477be | ||
|
|
dc13251bcc | ||
|
|
4843658dfb | ||
|
|
076afdc00e | ||
|
|
4404699fa6 | ||
|
|
52940ce254 | ||
|
|
cd6293d220 | ||
|
|
108f95723b | ||
|
|
f611c3d767 | ||
|
|
6091208f8a | ||
|
|
585a12fa48 | ||
|
|
bfc4f7b66c | ||
|
|
9dca13305f | ||
|
|
0ab9823c9a |
@@ -17,6 +17,10 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
- uses: lingodotdev/lingo.dev@main
|
- uses: lingodotdev/lingo.dev@main
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|||||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -488,13 +488,8 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
|
|||||||
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||||
|
|
||||||
## Single Org Mode
|
## Single Org Mode
|
||||||
If you want to have booker.yourcompany.com to be the domain used for both dashboard(e.g. https://booker.yourcompany.com/event-types) and booking pages(e.g. https://booker.yourcompany.com/john.joe/15min).
|
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
|
||||||
- Set the `NEXT_PUBLIC_SINGLE_ORG_SLUG` environment variable to the slug of the organization you want to use. `NEXT_PUBLIC_SINGLE_ORG_SLUG=booker`
|
|
||||||
- Set the `NEXT_PUBLIC_WEBAPP_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBAPP_URL=https://booker.yourcompany.com`.
|
|
||||||
- Set the `NEXT_PUBLIC_WEBSITE_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBSITE_URL=https://booker.yourcompany.com`.
|
|
||||||
- Set the `NEXTAUTH_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXTAUTH_URL=https://booker.yourcompany.com`.
|
|
||||||
|
|
||||||
Note: It causes root to serve the dashboard and not the organization profile page which shows all bookable users in the organization.
|
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
const mockDeploymentRepository: IDeploymentRepository = {
|
const mockDeploymentRepository: IDeploymentRepository = {
|
||||||
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
||||||
|
getSignatureToken: vi.fn().mockResolvedValue("mockSignatureToken"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Fix the skip condition for this test suite
|
// TODO: Fix the skip condition for this test suite
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum locales {
|
|||||||
SR = "sr",
|
SR = "sr",
|
||||||
SV = "sv",
|
SV = "sv",
|
||||||
VI = "vi",
|
VI = "vi",
|
||||||
|
BN = "bn",
|
||||||
}
|
}
|
||||||
enum theme {
|
enum theme {
|
||||||
DARK = "dark",
|
DARK = "dark",
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
|
|
||||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||||
|
|
||||||
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
|
const usersWithCalendars = await new UserRepository(
|
||||||
|
prisma
|
||||||
|
).findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||||
ids: userIds,
|
ids: userIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||||
|
import { getAvailableSlotsService } from "@calcom/lib/di/containers/available-slots";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types";
|
import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
|
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||||
@@ -27,7 +27,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
|
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
|
||||||
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
||||||
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
|
||||||
|
const availableSlotsService = getAvailableSlotsService();
|
||||||
|
const availableSlots = await availableSlotsService.getAvailableSlots({
|
||||||
|
ctx: await createContext({ req, res }),
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
|
||||||
const slotsInProvidedTimeZone = timeZoneSupported
|
const slotsInProvidedTimeZone = timeZoneSupported
|
||||||
? Object.keys(availableSlots.slots).reduce(
|
? Object.keys(availableSlots.slots).reduce(
|
||||||
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
watchAttempts: 0,
|
watchAttempts: 0,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
@@ -136,6 +138,8 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
watchAttempts: 0,
|
watchAttempts: 0,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.239",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.257",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
@Permissions([BOOKING_READ])
|
@Permissions([BOOKING_READ])
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get 'Booking References' for a booking",
|
summary: "Get booking references",
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getBookingReferences(
|
async getBookingReferences(
|
||||||
|
|||||||
@@ -1425,8 +1425,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
expect(data.status).toEqual("cancelled");
|
expect(data.status).toEqual("cancelled");
|
||||||
|
expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid);
|
||||||
createdBooking = data;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -596,9 +596,9 @@ export class BookingsService_2024_08_13 {
|
|||||||
} else if (isRecurring && isSeated) {
|
} else if (isRecurring && isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
||||||
} else if (isSeated) {
|
} else if (isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputSeatedBooking(formatted));
|
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
|
||||||
} else {
|
} else {
|
||||||
formattedBookings.push(this.outputService.getOutputBooking(formatted));
|
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,9 +190,20 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRoutingFormData(routing: { teamMemberIds?: number[]; responseId?: number; teamMemberEmail?: string; skipContactOwner?: boolean; crmAppSlug?: string; crmOwnerRecordType?: string } | undefined) {
|
private getRoutingFormData(
|
||||||
|
routing:
|
||||||
|
| {
|
||||||
|
teamMemberIds?: number[];
|
||||||
|
responseId?: number;
|
||||||
|
teamMemberEmail?: string;
|
||||||
|
skipContactOwner?: boolean;
|
||||||
|
crmAppSlug?: string;
|
||||||
|
crmOwnerRecordType?: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
) {
|
||||||
if (!routing) return null;
|
if (!routing) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
routedTeamMemberIds: routing.teamMemberIds,
|
routedTeamMemberIds: routing.teamMemberIds,
|
||||||
routingFormResponseId: routing.responseId,
|
routingFormResponseId: routing.responseId,
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
|
|||||||
export class OutputBookingsService_2024_08_13 {
|
export class OutputBookingsService_2024_08_13 {
|
||||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||||
|
|
||||||
getOutputBooking(databaseBooking: DatabaseBooking) {
|
async getOutputBooking(databaseBooking: DatabaseBooking) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
@@ -101,6 +101,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
);
|
);
|
||||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||||
|
const rescheduledToUid = databaseBooking.rescheduled
|
||||||
|
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
@@ -109,8 +112,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
|
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
@@ -137,6 +141,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
updatedAt: databaseBooking.updatedAt,
|
updatedAt: databaseBooking.updatedAt,
|
||||||
rating: databaseBooking.rating,
|
rating: databaseBooking.rating,
|
||||||
icsUid: databaseBooking.iCalUID,
|
icsUid: databaseBooking.iCalUID,
|
||||||
|
rescheduledToUid,
|
||||||
};
|
};
|
||||||
|
|
||||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||||
@@ -146,6 +151,11 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
return bookingTransformed;
|
return bookingTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRescheduledToUid(bookingUid: string) {
|
||||||
|
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||||
|
return rescheduledTo?.uid;
|
||||||
|
}
|
||||||
|
|
||||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||||
if (databaseMetadata === null) return {};
|
if (databaseMetadata === null) return {};
|
||||||
|
|
||||||
@@ -204,8 +214,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
|
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
@@ -244,20 +255,23 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
return bookingTransformed;
|
return bookingTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputCreateSeatedBooking(
|
async getOutputCreateSeatedBooking(
|
||||||
databaseBooking: DatabaseBooking,
|
databaseBooking: DatabaseBooking,
|
||||||
seatUid: string
|
seatUid: string
|
||||||
): CreateSeatedBookingOutput_2024_08_13 {
|
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
||||||
const getSeatedBookingOutput = this.getOutputSeatedBooking(databaseBooking);
|
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
|
||||||
return { ...getSeatedBookingOutput, seatUid };
|
return { ...getSeatedBookingOutput, seatUid };
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||||
|
const rescheduledToUid = databaseBooking.rescheduled
|
||||||
|
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
@@ -266,7 +280,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
start: databaseBooking.startTime,
|
start: databaseBooking.startTime,
|
||||||
end: databaseBooking.endTime,
|
end: databaseBooking.endTime,
|
||||||
@@ -283,6 +298,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
updatedAt: databaseBooking.updatedAt,
|
updatedAt: databaseBooking.updatedAt,
|
||||||
rating: databaseBooking.rating,
|
rating: databaseBooking.rating,
|
||||||
icsUid: databaseBooking.iCalUID,
|
icsUid: databaseBooking.iCalUID,
|
||||||
|
rescheduledToUid,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||||
@@ -375,7 +391,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
start: databaseBooking.startTime,
|
start: databaseBooking.startTime,
|
||||||
end: databaseBooking.endTime,
|
end: databaseBooking.endTime,
|
||||||
|
|||||||
+3
-1
@@ -124,7 +124,9 @@ export class EventTypesController_2024_04_15 {
|
|||||||
let orgSlug = queryParams.org;
|
let orgSlug = queryParams.org;
|
||||||
|
|
||||||
if (clientId && !orgSlug && username.includes(`-${clientId}`)) {
|
if (clientId && !orgSlug && username.includes(`-${clientId}`)) {
|
||||||
const org = await this.organizationsRepository.findTeamIdAndSlugFromClientId(clientId).catch(() => null);
|
const org = await this.organizationsRepository
|
||||||
|
.findTeamIdAndSlugFromClientId(clientId)
|
||||||
|
.catch(() => null);
|
||||||
if (org) {
|
if (org) {
|
||||||
orgSlug = org.slug;
|
orgSlug = org.slug;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export enum AuthMethods {
|
|||||||
"ACCESS_TOKEN" = "access-token",
|
"ACCESS_TOKEN" = "access-token",
|
||||||
"OAUTH_CLIENT" = "oauth-client",
|
"OAUTH_CLIENT" = "oauth-client",
|
||||||
"NEXT_AUTH" = "next-auth",
|
"NEXT_AUTH" = "next-auth",
|
||||||
|
"THIRD_PARTY_ACCESS_TOKEN" = "third_party_access_token",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ export enum Locales {
|
|||||||
TA = "ta",
|
TA = "ta",
|
||||||
UK = "uk",
|
UK = "uk",
|
||||||
ZH_TW = "zh-TW",
|
ZH_TW = "zh-TW",
|
||||||
|
BN = "bn",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
|
import { PrismaSelectedSlotsRepository } from "@/lib/repositories/prisma-selected-slots.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
|
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [
|
||||||
|
PrismaOOORepository,
|
||||||
|
PrismaScheduleRepository,
|
||||||
|
PrismaBookingRepository,
|
||||||
|
PrismaSelectedSlotsRepository,
|
||||||
|
PrismaUserRepository,
|
||||||
|
PrismaEventTypeRepository,
|
||||||
|
PrismaRoutingFormResponseRepository,
|
||||||
|
PrismaTeamRepository,
|
||||||
|
AvailableSlotsService,
|
||||||
|
],
|
||||||
|
exports: [AvailableSlotsService],
|
||||||
|
})
|
||||||
|
export class AvailableSlotsModule {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaBookingRepository as PrismaBookingRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaBookingRepository extends PrismaBookingRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaEventTypeRepository as PrismaEventTypeRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaEventTypeRepository extends PrismaEventTypeRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaOOORepository as PrismaOOORepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaOOORepository extends PrismaOOORepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaRoutingFormResponseRepository as PrismaRoutingFormResponseRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaRoutingFormResponseRepository extends PrismaRoutingFormResponseRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaScheduleRepository as PrismaScheduleRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaScheduleRepository extends PrismaScheduleRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaSelectedSlotsRepository as PrismaSelectedSlotsRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaSelectedSlotsRepository extends PrismaSelectedSlotsRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaTeamRepository as PrismaTeamRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaTeamRepository extends PrismaTeamRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaUserRepository as PrismaUserRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaUserRepository extends PrismaUserRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
|
import { PrismaSelectedSlotsRepository } from "@/lib/repositories/prisma-selected-slots.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||||
|
constructor(
|
||||||
|
private readonly oooRepoDependency: PrismaOOORepository,
|
||||||
|
private readonly scheduleRepoDependency: PrismaScheduleRepository,
|
||||||
|
private readonly teamRepository: PrismaTeamRepository,
|
||||||
|
private readonly routingFormResponseRepository: PrismaRoutingFormResponseRepository,
|
||||||
|
private readonly bookingRepository: PrismaBookingRepository,
|
||||||
|
private readonly selectedSlotsRepository: PrismaSelectedSlotsRepository,
|
||||||
|
private readonly eventTypeRepository: PrismaEventTypeRepository,
|
||||||
|
private readonly userRepository: PrismaUserRepository
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
oooRepo: oooRepoDependency,
|
||||||
|
scheduleRepo: scheduleRepoDependency,
|
||||||
|
teamRepo: teamRepository,
|
||||||
|
routingFormResponseRepo: routingFormResponseRepository,
|
||||||
|
bookingRepo: bookingRepository,
|
||||||
|
selectedSlotsRepo: selectedSlotsRepository,
|
||||||
|
eventTypeRepo: eventTypeRepository,
|
||||||
|
userRepo: userRepository,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
export type AllowedAuthMethod = "OAUTH_CLIENT_CREDENTIALS" | "API_KEY" | "ACCESS_TOKEN" | "NEXT_AUTH";
|
export type AllowedAuthMethod =
|
||||||
|
| "OAUTH_CLIENT_CREDENTIALS"
|
||||||
|
| "API_KEY"
|
||||||
|
| "ACCESS_TOKEN"
|
||||||
|
| "NEXT_AUTH"
|
||||||
|
| "THIRD_PARTY_ACCESS_TOKEN";
|
||||||
|
|
||||||
export const ApiAuthGuardOnlyAllow = Reflector.createDecorator<AllowedAuthMethod[]>();
|
export const ApiAuthGuardOnlyAllow = Reflector.createDecorator<AllowedAuthMethod[]>();
|
||||||
|
|||||||
@@ -10,16 +10,30 @@ import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
|||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
import {
|
||||||
|
HttpException,
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Logger } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
import * as jwt from "jsonwebtoken";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||||
|
|
||||||
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
|
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
|
||||||
|
|
||||||
|
interface OAuthTokenPayload {
|
||||||
|
userId?: number;
|
||||||
|
teamId?: number;
|
||||||
|
scope: string[];
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
||||||
export type ApiAuthGuardRequest = Request & {
|
export type ApiAuthGuardRequest = Request & {
|
||||||
authMethod: AuthMethods;
|
authMethod: AuthMethods;
|
||||||
@@ -29,8 +43,11 @@ export type ApiAuthGuardRequest = Request & {
|
|||||||
};
|
};
|
||||||
export const NO_AUTH_PROVIDED_MESSAGE =
|
export const NO_AUTH_PROVIDED_MESSAGE =
|
||||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||||
|
private readonly logger = new Logger("ApiAuthStrategy");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly deploymentsService: DeploymentsService,
|
private readonly deploymentsService: DeploymentsService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -54,22 +71,53 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
|
|
||||||
const allowedMethods = request.allowedAuthMethods;
|
const allowedMethods = request.allowedAuthMethods;
|
||||||
const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length;
|
const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length;
|
||||||
|
|
||||||
const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS");
|
const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS");
|
||||||
const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY");
|
const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY");
|
||||||
const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN");
|
const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN");
|
||||||
const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH");
|
const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH");
|
||||||
|
const thirdPartyAccessTokenAllowed =
|
||||||
|
noSpecificAuthExpected || allowedMethods.includes("THIRD_PARTY_ACCESS_TOKEN");
|
||||||
|
|
||||||
if (oAuthClientId && oAuthClientSecret && oAuthAllowed) {
|
if (oAuthClientId && oAuthClientSecret && oAuthAllowed) {
|
||||||
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
||||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bearerToken && (apiKeyAllowed || accessTokenAllowed)) {
|
if (bearerToken) {
|
||||||
const requestOrigin = request.get("Origin");
|
if (!apiKeyAllowed && !accessTokenAllowed && thirdPartyAccessTokenAllowed) {
|
||||||
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
request.authMethod = AuthMethods["THIRD_PARTY_ACCESS_TOKEN"];
|
||||||
? AuthMethods["API_KEY"]
|
const result = await this.validateThirdPartyAccessToken(bearerToken, request);
|
||||||
: AuthMethods["ACCESS_TOKEN"];
|
if (result.success) {
|
||||||
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
return this.success(this.getSuccessUser(result.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKeyAllowed || accessTokenAllowed) {
|
||||||
|
try {
|
||||||
|
const requestOrigin = request.get("Origin");
|
||||||
|
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||||
|
? AuthMethods["API_KEY"]
|
||||||
|
: AuthMethods["ACCESS_TOKEN"];
|
||||||
|
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
||||||
|
} catch (err) {
|
||||||
|
// failed to validate access token, try to validate third party token
|
||||||
|
if (thirdPartyAccessTokenAllowed && request.authMethod === AuthMethods["ACCESS_TOKEN"]) {
|
||||||
|
request.authMethod = AuthMethods["THIRD_PARTY_ACCESS_TOKEN"];
|
||||||
|
const result = await this.validateThirdPartyAccessToken(bearerToken, request);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return this.success(this.getSuccessUser(result.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// token was not third party token, rethrow error from authenticateBearerToken
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return this.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnauthorizedException(`ApiAuthStrategy - Invalid Bearer token`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||||
@@ -83,8 +131,11 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
if (noAuthProvided) {
|
if (noAuthProvided) {
|
||||||
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException(
|
||||||
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${allowedMethods}`
|
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${
|
||||||
|
allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method"
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
@@ -164,21 +215,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
: await this.accessTokenStrategy(authString, request, requestOrigin);
|
: await this.accessTokenStrategy(authString, request, requestOrigin);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return this.error(
|
throw new UnauthorizedException(
|
||||||
new UnauthorizedException(
|
"ApiAuthStrategy - bearer token - No user associated with the provided token"
|
||||||
"ApiAuthStrategy - bearer token - No user associated with the provided token"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.success(this.getSuccessUser(user));
|
return this.success(this.getSuccessUser(user));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
return this.error(err);
|
throw err;
|
||||||
}
|
}
|
||||||
return this.error(
|
throw new InternalServerErrorException("An error occurred while authenticating the request");
|
||||||
new InternalServerErrorException("An error occurred while authenticating the request")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,4 +318,55 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateThirdPartyAccessToken(
|
||||||
|
token: string,
|
||||||
|
request: ApiAuthGuardRequest
|
||||||
|
): Promise<{ success: true; data: UserWithProfile } | { success: false }> {
|
||||||
|
// Removed requiredScopes parameter
|
||||||
|
const encryptionKey = this.config.get<string>("CALENDSO_ENCRYPTION_KEY");
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new InternalServerErrorException("CALENDSO_ENCRYPTION_KEY environment variable is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodedToken: OAuthTokenPayload;
|
||||||
|
try {
|
||||||
|
decodedToken = jwt.verify(token, encryptionKey) as OAuthTokenPayload;
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decodedToken || decodedToken.token_type !== "Access Token") {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: UserWithProfile | null = null;
|
||||||
|
let organizationId: number | null = null;
|
||||||
|
|
||||||
|
if (decodedToken.userId) {
|
||||||
|
user = await this.userRepository.findByIdWithProfile(decodedToken.userId);
|
||||||
|
if (user) {
|
||||||
|
organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||||
|
}
|
||||||
|
} else if (decodedToken.teamId) {
|
||||||
|
const teamOwner = await this.userRepository.findOwnerByTeamIdWithProfile(decodedToken.teamId);
|
||||||
|
if (!teamOwner) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"ApiAuthStrategy - third-party token - No owner found for the associated team."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
user = teamOwner;
|
||||||
|
organizationId =
|
||||||
|
teamOwner.profiles?.find((p) => p.organizationId === decodedToken.teamId)?.organizationId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"ApiAuthStrategy - third-party token - No user or team owner associated with the token."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.organizationId = organizationId;
|
||||||
|
return { success: true, data: user };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ export class CalUnifiedCalendarsController {
|
|||||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get meeting Details from calendar",
|
summary: "Get meeting details from calendar",
|
||||||
description: "Returns detailed information about a meeting including attendance metrics",
|
description: "Returns detailed information about a meeting including attendance metrics",
|
||||||
})
|
})
|
||||||
async getCalendarEventDetails(
|
async getCalendarEventDetails(
|
||||||
|
|||||||
+4
@@ -92,6 +92,8 @@ describe("Platform Destination Calendar Endpoints", () => {
|
|||||||
publisher: "",
|
publisher: "",
|
||||||
url: "",
|
url: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
// calendars: {
|
// calendars: {
|
||||||
// externalId:
|
// externalId:
|
||||||
@@ -118,6 +120,8 @@ describe("Platform Destination Calendar Endpoints", () => {
|
|||||||
id: 0,
|
id: 0,
|
||||||
delegationCredentialId: null,
|
delegationCredentialId: null,
|
||||||
domainWideDelegationCredentialId: null,
|
domainWideDelegationCredentialId: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
+12
-6
@@ -304,8 +304,10 @@ describe("Assign all team members", () => {
|
|||||||
expect(data.title).toEqual(body.title);
|
expect(data.title).toEqual(body.title);
|
||||||
expect(data.hosts.length).toEqual(2);
|
expect(data.hosts.length).toEqual(2);
|
||||||
expect(data.schedulingType).toEqual("collective");
|
expect(data.schedulingType).toEqual("collective");
|
||||||
evaluateHost({ userId: firstManagedUser.user.id }, data.hosts[0]);
|
const dataFirstHost = data.hosts.find((host) => host.userId === firstManagedUser.user.id);
|
||||||
evaluateHost({ userId: secondManagedUser.user.id }, data.hosts[1]);
|
const dataSecondHost = data.hosts.find((host) => host.userId === secondManagedUser.user.id);
|
||||||
|
evaluateHost({ userId: firstManagedUser.user.id }, dataFirstHost);
|
||||||
|
evaluateHost({ userId: secondManagedUser.user.id }, dataSecondHost);
|
||||||
|
|
||||||
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
||||||
expect(eventTypeHosts.length).toEqual(2);
|
expect(eventTypeHosts.length).toEqual(2);
|
||||||
@@ -341,13 +343,15 @@ describe("Assign all team members", () => {
|
|||||||
expect(data.title).toEqual(body.title);
|
expect(data.title).toEqual(body.title);
|
||||||
expect(data.hosts.length).toEqual(2);
|
expect(data.hosts.length).toEqual(2);
|
||||||
expect(data.schedulingType).toEqual("roundRobin");
|
expect(data.schedulingType).toEqual("roundRobin");
|
||||||
|
const dataFirstHost = data.hosts.find((host) => host.userId === firstManagedUser.user.id);
|
||||||
|
const dataSecondHost = data.hosts.find((host) => host.userId === secondManagedUser.user.id);
|
||||||
evaluateHost(
|
evaluateHost(
|
||||||
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
|
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||||
data.hosts[0]
|
dataFirstHost
|
||||||
);
|
);
|
||||||
evaluateHost(
|
evaluateHost(
|
||||||
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
|
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||||
data.hosts[1]
|
dataSecondHost
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
||||||
@@ -381,13 +385,15 @@ describe("Assign all team members", () => {
|
|||||||
expect(data.title).toEqual(body.title);
|
expect(data.title).toEqual(body.title);
|
||||||
expect(data.hosts.length).toEqual(2);
|
expect(data.hosts.length).toEqual(2);
|
||||||
expect(data.schedulingType).toEqual("roundRobin");
|
expect(data.schedulingType).toEqual("roundRobin");
|
||||||
|
const dataFirstHost = data.hosts.find((host) => host.userId === firstManagedUser.user.id);
|
||||||
|
const dataSecondHost = data.hosts.find((host) => host.userId === secondManagedUser.user.id);
|
||||||
evaluateHost(
|
evaluateHost(
|
||||||
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
|
{ userId: firstManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||||
data.hosts[0]
|
dataFirstHost
|
||||||
);
|
);
|
||||||
evaluateHost(
|
evaluateHost(
|
||||||
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
|
{ userId: secondManagedUser.user.id, mandatory: false, priority: "medium" },
|
||||||
data.hosts[1]
|
dataSecondHost
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id);
|
||||||
|
|||||||
+83
-55
@@ -43,7 +43,7 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
teamId: number | null;
|
teamId: number | null;
|
||||||
userId: number | null;
|
userId: number | null;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
};
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
@@ -130,24 +130,24 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
operator: "equal",
|
operator: "equal",
|
||||||
value: ["answer1"],
|
value: ["answer1"],
|
||||||
valueSrc: ["value"],
|
valueSrc: ["value"],
|
||||||
valueType: ["text"]
|
valueType: ["text"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: "eventTypeRedirectUrl",
|
type: "eventTypeRedirectUrl",
|
||||||
eventTypeId: routingEventType.id,
|
eventTypeId: routingEventType.id,
|
||||||
value: `team/${team.slug}/${routingEventType.slug}`
|
value: `team/${team.slug}/${routingEventType.slug}`,
|
||||||
},
|
},
|
||||||
isFallback: false
|
isFallback: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fallback-route",
|
id: "fallback-route",
|
||||||
action: { type: "customPageMessage", value: "Fallback Message" },
|
action: { type: "customPageMessage", value: "Fallback Message" },
|
||||||
isFallback: true,
|
isFallback: true,
|
||||||
queryValue: { id: "fallback-route", type: "group" }
|
queryValue: { id: "fallback-route", type: "group" },
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -155,18 +155,18 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
label: "Question 1",
|
label: "Question 1",
|
||||||
required: true,
|
required: true,
|
||||||
identifier: "question1"
|
identifier: "question1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "question2",
|
id: "question2",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Question 2",
|
label: "Question 2",
|
||||||
required: false,
|
required: false,
|
||||||
identifier: "question2"
|
identifier: "question2",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
emailOwnerOnSubmission: false
|
emailOwnerOnSubmission: false,
|
||||||
},
|
},
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -245,7 +245,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
describe(`POST /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => {
|
describe(`POST /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => {
|
||||||
it("should return 403 when organization does not exist", async () => {
|
it("should return 403 when organization does not exist", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/99999/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/99999/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
@@ -255,7 +257,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should return 404 when routing form does not exist", async () => {
|
it("should return 404 when routing form does not exist", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/non-existent-id/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/non-existent-id/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
@@ -265,7 +269,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should return 401 when authentication token is missing", async () => {
|
it("should return 401 when authentication token is missing", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
})
|
})
|
||||||
@@ -274,7 +280,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should create response and return available slots when routing to event type", async () => {
|
it("should create response and return available slots when routing to event type", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1", // This matches the route condition
|
question1: "answer1", // This matches the route condition
|
||||||
@@ -296,7 +304,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should return 400 when required form fields are missing", async () => {
|
it("should return 400 when required form fields are missing", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question2: "answer2", // Missing required question1
|
question2: "answer2", // Missing required question1
|
||||||
@@ -306,7 +316,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should create response and return custom message if the routing is to custom page", async () => {
|
it("should create response and return custom message if the routing is to custom page", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "different-answer", // This won't match any route
|
question1: "different-answer", // This won't match any route
|
||||||
@@ -334,7 +346,7 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
})
|
})
|
||||||
.expect(400);
|
.expect(400);
|
||||||
|
|
||||||
// Missing end parameter
|
// Missing end parameter
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05`)
|
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05`)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
@@ -347,7 +359,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should return 400 when date parameters have invalid format", async () => {
|
it("should return 400 when date parameters have invalid format", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=invalid-date&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=invalid-date&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
@@ -357,7 +371,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should return 400 when end date is before start date", async () => {
|
it("should return 400 when end date is before start date", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-10&end=2050-09-05`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-10&end=2050-09-05`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
@@ -371,10 +387,15 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
email: `unauthorized-user-${randomString()}@api.com`,
|
email: `unauthorized-user-${randomString()}@api.com`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { keyString: unauthorizedApiKey } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
|
const { keyString: unauthorizedApiKey } = await apiKeysRepositoryFixture.createApiKey(
|
||||||
|
unauthorizedUser.id,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${unauthorizedApiKey}` })
|
.set({ Authorization: `Bearer cal_test_${unauthorizedApiKey}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
@@ -390,7 +411,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
it("should handle queued response creation", async () => {
|
it("should handle queued response creation", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06&queueResponse=true`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06&queueResponse=true`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
@@ -426,24 +449,24 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
operator: "equal",
|
operator: "equal",
|
||||||
value: ["answer1"],
|
value: ["answer1"],
|
||||||
valueSrc: ["value"],
|
valueSrc: ["value"],
|
||||||
valueType: ["text"]
|
valueType: ["text"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: "eventTypeRedirectUrl",
|
type: "eventTypeRedirectUrl",
|
||||||
eventTypeId: 99999, // Invalid event type ID
|
eventTypeId: 99999, // Invalid event type ID
|
||||||
value: `team/${team.slug}/non-existent-event-type`
|
value: `team/${team.slug}/non-existent-event-type`,
|
||||||
},
|
},
|
||||||
isFallback: false
|
isFallback: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fallback-route",
|
id: "fallback-route",
|
||||||
action: { type: "customPageMessage", value: "Fallback Message" },
|
action: { type: "customPageMessage", value: "Fallback Message" },
|
||||||
isFallback: true,
|
isFallback: true,
|
||||||
queryValue: { id: "fallback-route", type: "group" }
|
queryValue: { id: "fallback-route", type: "group" },
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -451,11 +474,11 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
label: "Question 1",
|
label: "Question 1",
|
||||||
required: true,
|
required: true,
|
||||||
identifier: "question1"
|
identifier: "question1",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
emailOwnerOnSubmission: false
|
emailOwnerOnSubmission: false,
|
||||||
},
|
},
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -464,7 +487,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
// Try to create a response for the form with invalid event type
|
// Try to create a response for the form with invalid event type
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingFormWithInvalidEventType.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingFormWithInvalidEventType.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1",
|
question1: "answer1",
|
||||||
@@ -474,7 +499,7 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
// Clean up the form
|
// Clean up the form
|
||||||
await prismaWriteService.prisma.app_RoutingForms_Form.delete({
|
await prismaWriteService.prisma.app_RoutingForms_Form.delete({
|
||||||
where: { id: routingFormWithInvalidEventType.id }
|
where: { id: routingFormWithInvalidEventType.id },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -482,7 +507,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
// This test verifies that routing forms can handle team member assignments
|
// This test verifies that routing forms can handle team member assignments
|
||||||
// and that the routing returns the correct team member information
|
// and that the routing returns the correct team member information
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "answer1", // This matches the route condition
|
question1: "answer1", // This matches the route condition
|
||||||
@@ -524,23 +551,23 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
operator: "equal",
|
operator: "equal",
|
||||||
value: ["external"],
|
value: ["external"],
|
||||||
valueSrc: ["value"],
|
valueSrc: ["value"],
|
||||||
valueType: ["text"]
|
valueType: ["text"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: "externalRedirectUrl",
|
type: "externalRedirectUrl",
|
||||||
value: "https://example.com/external-booking"
|
value: "https://example.com/external-booking",
|
||||||
},
|
},
|
||||||
isFallback: false
|
isFallback: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fallback-route",
|
id: "fallback-route",
|
||||||
action: { type: "customPageMessage", value: "Fallback Message" },
|
action: { type: "customPageMessage", value: "Fallback Message" },
|
||||||
isFallback: true,
|
isFallback: true,
|
||||||
queryValue: { id: "fallback-route", type: "group" }
|
queryValue: { id: "fallback-route", type: "group" },
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -548,11 +575,11 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
label: "Question 1",
|
label: "Question 1",
|
||||||
required: true,
|
required: true,
|
||||||
identifier: "question1"
|
identifier: "question1",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
emailOwnerOnSubmission: false
|
emailOwnerOnSubmission: false,
|
||||||
},
|
},
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -560,7 +587,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/routing-forms/${externalRoutingForm.id}/responses?start=2050-09-05&end=2050-09-06`)
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/routing-forms/${externalRoutingForm.id}/responses?start=2050-09-05&end=2050-09-06`
|
||||||
|
)
|
||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.send({
|
.send({
|
||||||
question1: "external", // This matches the route condition for external redirect
|
question1: "external", // This matches the route condition for external redirect
|
||||||
@@ -574,7 +603,7 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
expect(data.routingExternalRedirectUrl).toBeDefined();
|
expect(data.routingExternalRedirectUrl).toBeDefined();
|
||||||
expect(data.routingExternalRedirectUrl).toContain("https://example.com/external-booking");
|
expect(data.routingExternalRedirectUrl).toContain("https://example.com/external-booking");
|
||||||
expect(data.routingExternalRedirectUrl).toContain("cal.action=externalRedirectUrl");
|
expect(data.routingExternalRedirectUrl).toContain("cal.action=externalRedirectUrl");
|
||||||
|
|
||||||
// Verify that it doesn't contain event type routing data
|
// Verify that it doesn't contain event type routing data
|
||||||
expect(data.eventTypeId).toBeUndefined();
|
expect(data.eventTypeId).toBeUndefined();
|
||||||
expect(data.slots).toBeUndefined();
|
expect(data.slots).toBeUndefined();
|
||||||
@@ -583,10 +612,9 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
|
|
||||||
// Clean up the external routing form
|
// Clean up the external routing form
|
||||||
await prismaWriteService.prisma.app_RoutingForms_Form.delete({
|
await prismaWriteService.prisma.app_RoutingForms_Form.delete({
|
||||||
where: { id: externalRoutingForm.id }
|
where: { id: externalRoutingForm.id },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`PATCH /v2/organizations/:orgId/routing-forms/:routingFormId/responses/:responseId`, () => {
|
describe(`PATCH /v2/organizations/:orgId/routing-forms/:routingFormId/responses/:responseId`, () => {
|
||||||
|
|||||||
+4
-4
@@ -6,9 +6,9 @@ import { GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types";
|
|||||||
|
|
||||||
export class CreateRoutingFormResponseInput extends GetAvailableSlotsInput_2024_09_04 {
|
export class CreateRoutingFormResponseInput extends GetAvailableSlotsInput_2024_09_04 {
|
||||||
@Transform(({ value }: { value: string | boolean }) => {
|
@Transform(({ value }: { value: string | boolean }) => {
|
||||||
if (typeof value === 'boolean') return value;
|
if (typeof value === "boolean") return value;
|
||||||
if (typeof value === 'string') {
|
if (typeof value === "string") {
|
||||||
return value.toLowerCase() === 'true';
|
return value.toLowerCase() === "true";
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
})
|
})
|
||||||
@@ -20,4 +20,4 @@ export class CreateRoutingFormResponseInput extends GetAvailableSlotsInput_2024_
|
|||||||
example: true,
|
example: true,
|
||||||
})
|
})
|
||||||
queueResponse?: boolean;
|
queueResponse?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -2,7 +2,12 @@ import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger
|
|||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { ApiResponseWithoutData, SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04 } from "@calcom/platform-types";
|
import {
|
||||||
|
ApiResponseWithoutData,
|
||||||
|
SlotsOutput_2024_09_04,
|
||||||
|
RangeSlotsOutput_2024_09_04,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
class Routing {
|
class Routing {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: String,
|
type: String,
|
||||||
@@ -133,4 +138,4 @@ export class CreateRoutingFormResponseOutput extends ApiResponseWithoutData {
|
|||||||
@ApiProperty({ type: CreateRoutingFormResponseOutputData })
|
@ApiProperty({ type: CreateRoutingFormResponseOutputData })
|
||||||
@Type(() => CreateRoutingFormResponseOutputData)
|
@Type(() => CreateRoutingFormResponseOutputData)
|
||||||
data!: CreateRoutingFormResponseOutputData;
|
data!: CreateRoutingFormResponseOutputData;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-14
@@ -2,7 +2,12 @@ import { OrganizationsRoutingFormsRepository } from "@/modules/organizations/rou
|
|||||||
import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service";
|
import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service";
|
||||||
import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service";
|
import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service";
|
||||||
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
||||||
import { BadRequestException, Injectable, NotFoundException, InternalServerErrorException } from "@nestjs/common";
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
InternalServerErrorException,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
import { getRoutedUrl } from "@calcom/platform-libraries";
|
import { getRoutedUrl } from "@calcom/platform-libraries";
|
||||||
@@ -71,10 +76,14 @@ export class OrganizationsRoutingFormsResponsesService {
|
|||||||
request: Request
|
request: Request
|
||||||
): Promise<CreateRoutingFormResponseOutputData> {
|
): Promise<CreateRoutingFormResponseOutputData> {
|
||||||
const { queueResponse, ...slotsQuery } = query;
|
const { queueResponse, ...slotsQuery } = query;
|
||||||
|
|
||||||
this.validateDateRange(slotsQuery.start, slotsQuery.end);
|
this.validateDateRange(slotsQuery.start, slotsQuery.end);
|
||||||
|
|
||||||
const { redirectUrl, customMessage } = await this.getRoutingUrl(request, routingFormId, queueResponse ?? false);
|
const { redirectUrl, customMessage } = await this.getRoutingUrl(
|
||||||
|
request,
|
||||||
|
routingFormId,
|
||||||
|
queueResponse ?? false
|
||||||
|
);
|
||||||
|
|
||||||
// If there is no redirect URL, then we have to show the message as that would be custom page message to be shown as per the route chosen
|
// If there is no redirect URL, then we have to show the message as that would be custom page message to be shown as per the route chosen
|
||||||
if (!redirectUrl) {
|
if (!redirectUrl) {
|
||||||
@@ -86,7 +95,7 @@ export class OrganizationsRoutingFormsResponsesService {
|
|||||||
if (!this.isEventTypeRedirectUrl(redirectUrl)) {
|
if (!this.isEventTypeRedirectUrl(redirectUrl)) {
|
||||||
return {
|
return {
|
||||||
routingExternalRedirectUrl: redirectUrl.toString(),
|
routingExternalRedirectUrl: redirectUrl.toString(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract event type information from the routed URL
|
// Extract event type information from the routed URL
|
||||||
@@ -123,9 +132,11 @@ export class OrganizationsRoutingFormsResponsesService {
|
|||||||
slots,
|
slots,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queuedResponseId) {
|
if (!queuedResponseId) {
|
||||||
throw new InternalServerErrorException("No routing form response ID or queued form response ID could be found.");
|
throw new InternalServerErrorException(
|
||||||
|
"No routing form response ID or queued form response ID could be found."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -145,7 +156,7 @@ export class OrganizationsRoutingFormsResponsesService {
|
|||||||
private validateDateRange(start: string, end: string) {
|
private validateDateRange(start: string, end: string) {
|
||||||
const startDate = new Date(start);
|
const startDate = new Date(start);
|
||||||
const endDate = new Date(end);
|
const endDate = new Date(end);
|
||||||
|
|
||||||
if (endDate < startDate) {
|
if (endDate < startDate) {
|
||||||
throw new BadRequestException("End date cannot be before start date.");
|
throw new BadRequestException("End date cannot be before start date.");
|
||||||
}
|
}
|
||||||
@@ -153,10 +164,13 @@ export class OrganizationsRoutingFormsResponsesService {
|
|||||||
|
|
||||||
private async getRoutingUrl(request: Request, formId: string, queueResponse: boolean) {
|
private async getRoutingUrl(request: Request, formId: string, queueResponse: boolean) {
|
||||||
const params = Object.fromEntries(new URLSearchParams(request.body));
|
const params = Object.fromEntries(new URLSearchParams(request.body));
|
||||||
const routedUrlData = await getRoutedUrl({
|
const routedUrlData = await getRoutedUrl(
|
||||||
req: request,
|
{
|
||||||
query: { ...params, form: formId, ...(queueResponse && { "cal.queueFormResponse": "true" }) },
|
req: request,
|
||||||
}, true);
|
query: { ...params, form: formId, ...(queueResponse && { "cal.queueFormResponse": "true" }) },
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
if (routedUrlData.notFound) {
|
if (routedUrlData.notFound) {
|
||||||
throw new NotFoundException("Routing form not found.");
|
throw new NotFoundException("Routing form not found.");
|
||||||
@@ -186,8 +200,6 @@ export class OrganizationsRoutingFormsResponsesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async extractEventTypeAndCrmParams(routingUrl: URL) {
|
private async extractEventTypeAndCrmParams(routingUrl: URL) {
|
||||||
|
|
||||||
|
|
||||||
// Extract team and event type information
|
// Extract team and event type information
|
||||||
const { teamId, eventTypeSlug } = this.extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl);
|
const { teamId, eventTypeSlug } = this.extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl);
|
||||||
const eventType = await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(teamId, eventTypeSlug);
|
const eventType = await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(teamId, eventTypeSlug);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export type OAuthCallbackState = {
|
|||||||
path: "/v2/organizations/:orgId/teams/:teamId/stripe",
|
path: "/v2/organizations/:orgId/teams/:teamId/stripe",
|
||||||
version: API_VERSIONS_VALUES,
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@DocsTags("Organizations/Teams Stripe")
|
@DocsTags("Orgs / Teams / Stripe")
|
||||||
export class OrganizationsStripeController {
|
export class OrganizationsStripeController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly organizationsStripeService: OrganizationsStripeService,
|
private readonly organizationsStripeService: OrganizationsStripeService,
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ export class OrganizationsTeamsBookingsController {
|
|||||||
IsAdminAPIEnabledGuard
|
IsAdminAPIEnabledGuard
|
||||||
)
|
)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get 'Booking References' for a booking",
|
summary: "Get booking references",
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getBookingReferences(
|
async getBookingReferences(
|
||||||
|
|||||||
+8
-1
@@ -20,7 +20,14 @@ import { OrganizationsTeamsRoutingFormsResponsesService } from "./services/organ
|
|||||||
import { OrganizationsTeamsRoutingFormsService } from "./services/organizations-teams-routing-forms.service";
|
import { OrganizationsTeamsRoutingFormsService } from "./services/organizations-teams-routing-forms.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, StripeModule, RedisModule, RoutingFormsModule, SlotsModule_2024_09_04, TeamsEventTypesModule],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
StripeModule,
|
||||||
|
RedisModule,
|
||||||
|
RoutingFormsModule,
|
||||||
|
SlotsModule_2024_09_04,
|
||||||
|
TeamsEventTypesModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
OrganizationsTeamsRoutingFormsService,
|
OrganizationsTeamsRoutingFormsService,
|
||||||
OrganizationsTeamsRoutingFormsResponsesService,
|
OrganizationsTeamsRoutingFormsResponsesService,
|
||||||
|
|||||||
+10
-10
@@ -52,8 +52,8 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
|||||||
export class OrgTeamsVerifiedResourcesController {
|
export class OrgTeamsVerifiedResourcesController {
|
||||||
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Request Email Verification Code",
|
summary: "Request email verification code",
|
||||||
description: `Sends a verification code to the email.`,
|
description: `Sends a verification code to the email`,
|
||||||
})
|
})
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@Throttle({
|
@Throttle({
|
||||||
@@ -82,8 +82,8 @@ export class OrgTeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Request Phone Number Verification Code",
|
summary: "Request phone number verification code",
|
||||||
description: `Sends a verification code to the phone number.`,
|
description: `Sends a verification code to the phone number`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@@ -108,7 +108,7 @@ export class OrgTeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Verify an email for an org team.",
|
summary: "Verify an email for an org team",
|
||||||
description: `Use code to verify an email`,
|
description: `Use code to verify an email`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -134,7 +134,7 @@ export class OrgTeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Verify a phone number for an org team.",
|
summary: "Verify a phone number for an org team",
|
||||||
description: `Use code to verify a phone number`,
|
description: `Use code to verify a phone number`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -166,7 +166,7 @@ export class OrgTeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get list of verified emails of an org team.",
|
summary: "Get list of verified emails of an org team",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@@ -189,7 +189,7 @@ export class OrgTeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get list of verified phone numbers of an org team.",
|
summary: "Get list of verified phone numbers of an org team",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@PlatformPlan("ESSENTIALS")
|
@PlatformPlan("ESSENTIALS")
|
||||||
@@ -214,7 +214,7 @@ export class OrgTeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get verified email of an org team by id.",
|
summary: "Get verified email of an org team by id",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@@ -233,7 +233,7 @@ export class OrgTeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get verified phone number of an org team by id.",
|
summary: "Get verified phone number of an org team by id",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export class RouterController {
|
|||||||
@Param("formId") formId: string,
|
@Param("formId") formId: string,
|
||||||
@Body() body?: Record<string, string>
|
@Body() body?: Record<string, string>
|
||||||
): Promise<void | (ApiResponse<unknown> & { redirect: boolean })> {
|
): Promise<void | (ApiResponse<unknown> & { redirect: boolean })> {
|
||||||
|
|
||||||
const params = Object.fromEntries(new URLSearchParams(body ?? {}));
|
const params = Object.fromEntries(new URLSearchParams(body ?? {}));
|
||||||
const routedUrlData = await getRoutedUrl({ req: request, query: { ...params, form: formId } });
|
const routedUrlData = await getRoutedUrl({ req: request, query: { ...params, form: formId } });
|
||||||
if (routedUrlData?.notFound) {
|
if (routedUrlData?.notFound) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TRPC_ERROR_CODE, TRPC_ERROR_MAP, TRPCErrorCode } from "@/filters/trpc-exception.filter";
|
import { TRPC_ERROR_CODE, TRPC_ERROR_MAP, TRPCErrorCode } from "@/filters/trpc-exception.filter";
|
||||||
|
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||||
import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
||||||
import type { RangeSlots, TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
import type { RangeSlots, TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
||||||
import { SlotsWorkerService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-worker.service";
|
import { SlotsWorkerService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-worker.service";
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
VERSION_2024_08_13,
|
VERSION_2024_08_13,
|
||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { TRPCError } from "@calcom/platform-libraries";
|
import { TRPCError } from "@calcom/platform-libraries";
|
||||||
import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
|
||||||
import { RemoveSelectedSlotInput_2024_04_15, ReserveSlotInput_2024_04_15 } from "@calcom/platform-types";
|
import { RemoveSelectedSlotInput_2024_04_15, ReserveSlotInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
import { ApiResponse, GetAvailableSlotsInput_2024_04_15 } from "@calcom/platform-types";
|
import { ApiResponse, GetAvailableSlotsInput_2024_04_15 } from "@calcom/platform-types";
|
||||||
|
|
||||||
@@ -31,7 +31,8 @@ export class SlotsController_2024_04_15 {
|
|||||||
private readonly slotsService: SlotsService_2024_04_15,
|
private readonly slotsService: SlotsService_2024_04_15,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly slotsOutputService: SlotsOutputService_2024_04_15,
|
private readonly slotsOutputService: SlotsOutputService_2024_04_15,
|
||||||
private readonly slotsWorkerService: SlotsWorkerService_2024_04_15
|
private readonly slotsWorkerService: SlotsWorkerService_2024_04_15,
|
||||||
|
private readonly availableSlotsService: AvailableSlotsService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/reserve")
|
@Post("/reserve")
|
||||||
@@ -170,8 +171,10 @@ export class SlotsController_2024_04_15 {
|
|||||||
: query.isTeamEvent;
|
: query.isTeamEvent;
|
||||||
|
|
||||||
// Do not use workers in E2E, not supported by TS-JEST
|
// Do not use workers in E2E, not supported by TS-JEST
|
||||||
const availableSlots = this.config.get<boolean>("e2e")
|
const availableSlotsService = this.config.get<boolean>("e2e") ? this.availableSlotsService : null;
|
||||||
? await getAvailableSlots({
|
|
||||||
|
const availableSlots = availableSlotsService
|
||||||
|
? await availableSlotsService.getAvailableSlots({
|
||||||
input: {
|
input: {
|
||||||
...query,
|
...query,
|
||||||
isTeamEvent,
|
isTeamEvent,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||||
|
import { AvailableSlotsModule } from "@/lib/modules/available-slots.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SlotsController_2024_04_15 } from "@/modules/slots/slots-2024-04-15/controllers/slots.controller";
|
import { SlotsController_2024_04_15 } from "@/modules/slots/slots-2024-04-15/controllers/slots.controller";
|
||||||
import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
||||||
@@ -8,7 +9,7 @@ import { SlotsRepository_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slo
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, EventTypesModule_2024_04_15],
|
imports: [PrismaModule, EventTypesModule_2024_04_15, AvailableSlotsModule],
|
||||||
providers: [
|
providers: [
|
||||||
SlotsRepository_2024_04_15,
|
SlotsRepository_2024_04_15,
|
||||||
SlotsService_2024_04_15,
|
SlotsService_2024_04_15,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// apps/api/v2/src/modules/slots/slots-2024-04-15/workers/slots-worker.module.ts
|
||||||
|
// Needed if ConfigService is used anywhere
|
||||||
|
import appConfig from "@/config/app";
|
||||||
|
import { AvailableSlotsModule } from "@/lib/modules/available-slots.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { Module, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
import { SentryModule } from "@sentry/nestjs/setup";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
SentryModule.forRoot(),
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
ignoreEnvFile: true,
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfig],
|
||||||
|
}),
|
||||||
|
PrismaModule,
|
||||||
|
AvailableSlotsModule,
|
||||||
|
],
|
||||||
|
providers: [Logger],
|
||||||
|
})
|
||||||
|
export class SlotsWorkerModule {}
|
||||||
@@ -1,16 +1,65 @@
|
|||||||
import { parentPort } from "worker_threads";
|
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||||
|
import { Logger } from "@nestjs/common";
|
||||||
|
import { NestFactory } from "@nestjs/core";
|
||||||
|
import { IncomingMessage } from "http";
|
||||||
|
import { parentPort, isMainThread } from "worker_threads";
|
||||||
|
|
||||||
import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
import { SlotsWorkerModule } from "./slots.worker.module";
|
||||||
|
|
||||||
parentPort?.on("message", async (data) => {
|
interface ContextForGetSchedule extends Record<string, unknown> {
|
||||||
try {
|
req?: (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }) | undefined;
|
||||||
const { input, ctx } = data;
|
}
|
||||||
const result = await getAvailableSlots({ input, ctx });
|
|
||||||
parentPort?.postMessage({ success: true, data: result });
|
interface WorkerMessage {
|
||||||
} catch (error) {
|
input: any; // Use a more specific type if available, e.g., TGetScheduleInputSchema
|
||||||
parentPort?.postMessage({
|
ctx?: ContextForGetSchedule;
|
||||||
success: false,
|
}
|
||||||
error: error instanceof Error ? error : new Error("Unknown error"),
|
|
||||||
});
|
interface WorkerResult {
|
||||||
|
success: boolean;
|
||||||
|
data?: any; // Use TimeSlots
|
||||||
|
error?: {
|
||||||
|
message: string;
|
||||||
|
code?: string;
|
||||||
|
stack?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrapWorkerApp() {
|
||||||
|
if (isMainThread) {
|
||||||
|
console.error("slots-worker-main.ts should only be run in a worker thread.");
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const app = await NestFactory.createApplicationContext(SlotsWorkerModule);
|
||||||
|
const logger = app.get(Logger); // Get the NestJS Logger instance
|
||||||
|
logger.log("NestJS worker application bootstrapped successfully.");
|
||||||
|
|
||||||
|
const availableSlotsService = app.get(AvailableSlotsService);
|
||||||
|
|
||||||
|
parentPort?.on("message", async (data: WorkerMessage) => {
|
||||||
|
try {
|
||||||
|
const { input, ctx } = data;
|
||||||
|
|
||||||
|
const result = await availableSlotsService.getAvailableSlots({ input, ctx });
|
||||||
|
parentPort?.postMessage({ success: true, data: result } as WorkerResult);
|
||||||
|
} catch (error: any) {
|
||||||
|
parentPort?.postMessage({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error("Unknown error"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parentPort?.on("close", async () => {
|
||||||
|
logger.log("Worker port closed. Shutting down NestJS worker application.");
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
parentPort?.on("disconnect", async () => {
|
||||||
|
logger.log("Worker port disconnected. Shutting down NestJS worker application.");
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapWorkerApp();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
|
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
|
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
|
||||||
import { TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
import { TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
|
||||||
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
|
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
|
||||||
import { Booking, EventType } from "@calcom/prisma/client";
|
import { Booking, EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -38,13 +38,14 @@ export class SlotsService_2024_09_04 {
|
|||||||
private readonly slotsInputService: SlotsInputService_2024_09_04,
|
private readonly slotsInputService: SlotsInputService_2024_09_04,
|
||||||
private readonly membershipsService: MembershipsService,
|
private readonly membershipsService: MembershipsService,
|
||||||
private readonly membershipsRepository: MembershipsRepository,
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
private readonly teamsRepository: TeamsRepository
|
private readonly teamsRepository: TeamsRepository,
|
||||||
|
private readonly availableSlotsService: AvailableSlotsService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getAvailableSlots(query: GetSlotsInput_2024_09_04) {
|
async getAvailableSlots(query: GetSlotsInput_2024_09_04) {
|
||||||
try {
|
try {
|
||||||
const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query);
|
const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query);
|
||||||
const availableSlots: TimeSlots = await getAvailableSlots({
|
const availableSlots: TimeSlots = await this.availableSlotsService.getAvailableSlots({
|
||||||
input: {
|
input: {
|
||||||
...queryTransformed,
|
...queryTransformed,
|
||||||
routingFormResponseId: queryTransformed.routingFormResponseId ?? undefined,
|
routingFormResponseId: queryTransformed.routingFormResponseId ?? undefined,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
|
import { AvailableSlotsModule } from "@/lib/modules/available-slots.module";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||||
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
|
|||||||
TeamsModule,
|
TeamsModule,
|
||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
TeamsEventTypesModule,
|
TeamsEventTypesModule,
|
||||||
|
AvailableSlotsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SlotsRepository_2024_09_04,
|
SlotsRepository_2024_09_04,
|
||||||
|
|||||||
+10
-10
@@ -47,8 +47,8 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
|||||||
export class TeamsVerifiedResourcesController {
|
export class TeamsVerifiedResourcesController {
|
||||||
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Request Email Verification Code",
|
summary: "Request email verification code",
|
||||||
description: `Sends a verification code to the Email.`,
|
description: `Sends a verification code to the Email`,
|
||||||
})
|
})
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -71,8 +71,8 @@ export class TeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Request Phone Number Verification Code",
|
summary: "Request phone number verification code",
|
||||||
description: `Sends a verification code to the phone number.`,
|
description: `Sends a verification code to the phone number`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@@ -92,7 +92,7 @@ export class TeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Verify an email for a team.",
|
summary: "Verify an email for a team",
|
||||||
description: `Use code to verify an email`,
|
description: `Use code to verify an email`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -117,7 +117,7 @@ export class TeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Verify a phone number for an org team.",
|
summary: "Verify a phone number for an org team",
|
||||||
description: `Use code to verify a phone number`,
|
description: `Use code to verify a phone number`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -143,7 +143,7 @@ export class TeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get list of verified emails of a team.",
|
summary: "Get list of verified emails of a team",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@@ -165,7 +165,7 @@ export class TeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get list of verified phone numbers of a team.",
|
summary: "Get list of verified phone numbers of a team",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Get("/phones")
|
@Get("/phones")
|
||||||
@@ -189,7 +189,7 @@ export class TeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get verified email of a team by id.",
|
summary: "Get verified email of a team by id",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@@ -207,7 +207,7 @@ export class TeamsVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get verified phone number of a team by id.",
|
summary: "Get verified phone number of a team by id",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
|
|||||||
@@ -85,6 +85,27 @@ export class UsersRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOwnerByTeamIdWithProfile(teamId: number): Promise<UserWithProfile | null> {
|
||||||
|
return this.dbRead.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
teams: {
|
||||||
|
some: {
|
||||||
|
teamId,
|
||||||
|
role: "OWNER",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
movedToProfile: {
|
||||||
|
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
|
||||||
|
},
|
||||||
|
profiles: {
|
||||||
|
include: { organization: { select: { isPlatform: true, name: true, slug: true, id: true } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findByIdsWithEventTypes(userIds: number[]) {
|
async findByIdsWithEventTypes(userIds: number[]) {
|
||||||
return this.dbRead.prisma.user.findMany({
|
return this.dbRead.prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export class LocaleValidator implements ValidatorConstraintInterface {
|
|||||||
"sr",
|
"sr",
|
||||||
"sv",
|
"sv",
|
||||||
"vi",
|
"vi",
|
||||||
|
"bn",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (localeValues.includes(locale)) return true;
|
if (localeValues.includes(locale)) return true;
|
||||||
|
|||||||
+10
-10
@@ -34,8 +34,8 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
|||||||
export class UserVerifiedResourcesController {
|
export class UserVerifiedResourcesController {
|
||||||
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Request Email Verification Code",
|
summary: "Request email verification code",
|
||||||
description: `Sends a verification code to the email.`,
|
description: `Sends a verification code to the email`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Post("/emails/verification-code/request")
|
@Post("/emails/verification-code/request")
|
||||||
@@ -57,8 +57,8 @@ export class UserVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Request Phone Number Verification Code",
|
summary: "Request phone number verification code",
|
||||||
description: `Sends a verification code to the phone number.`,
|
description: `Sends a verification code to the phone number`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Post("/phones/verification-code/request")
|
@Post("/phones/verification-code/request")
|
||||||
@@ -77,7 +77,7 @@ export class UserVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Verify an email.",
|
summary: "Verify an email",
|
||||||
description: `Use code to verify an email`,
|
description: `Use code to verify an email`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -96,7 +96,7 @@ export class UserVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Verify a phone number.",
|
summary: "Verify a phone number",
|
||||||
description: `Use code to verify a phone number`,
|
description: `Use code to verify a phone number`,
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -114,7 +114,7 @@ export class UserVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get list of verified emails.",
|
summary: "Get list of verified emails",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Get("/emails")
|
@Get("/emails")
|
||||||
@@ -135,7 +135,7 @@ export class UserVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get list of verified phone numbers.",
|
summary: "Get list of verified phone numbers",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Get("/phones")
|
@Get("/phones")
|
||||||
@@ -158,7 +158,7 @@ export class UserVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get verified email by id.",
|
summary: "Get verified email by id",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Get("/emails/:id")
|
@Get("/emails/:id")
|
||||||
@@ -175,7 +175,7 @@ export class UserVerifiedResourcesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get verified phone number by id.",
|
summary: "Get verified phone number by id",
|
||||||
})
|
})
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Get("/phones/:id")
|
@Get("/phones/:id")
|
||||||
|
|||||||
@@ -3447,7 +3447,7 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/bookings/{bookingUid}/references": {
|
"/v2/organizations/{orgId}/teams/{teamId}/bookings/{bookingUid}/references": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsTeamsBookingsController_getBookingReferences",
|
"operationId": "OrganizationsTeamsBookingsController_getBookingReferences",
|
||||||
"summary": "Get 'Booking References' for a booking",
|
"summary": "Get booking references",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
@@ -5133,6 +5133,147 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
|
||||||
|
"summary": "Get Stripe connect URL for a team",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"required": true,
|
||||||
|
"in": "header",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "orgId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "returnTo",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "onErrorReturnTo",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StripConnectOutputResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Stripe"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/stripe/check": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsStripeController_checkTeamStripeConnection",
|
||||||
|
"summary": "Check team Stripe connection",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StripCredentialsCheckOutputResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Stripe"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/stripe/save": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsStripeController_save",
|
||||||
|
"summary": "Save Stripe credentials",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "state",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StripCredentialsSaveOutputResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Stripe"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": {
|
"/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsTeamsSchedulesController_getUserSchedules",
|
"operationId": "OrganizationsTeamsSchedulesController_getUserSchedules",
|
||||||
@@ -8176,7 +8317,7 @@
|
|||||||
"/v2/bookings/{bookingUid}/references": {
|
"/v2/bookings/{bookingUid}/references": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "BookingsController_2024_08_13_getBookingReferences",
|
"operationId": "BookingsController_2024_08_13_getBookingReferences",
|
||||||
"summary": "Get 'Booking References' for a booking",
|
"summary": "Get booking references",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "cal-api-version",
|
"name": "cal-api-version",
|
||||||
@@ -8244,7 +8385,7 @@
|
|||||||
"/v2/calendars/{calendar}/event/{eventUid}": {
|
"/v2/calendars/{calendar}/event/{eventUid}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "CalUnifiedCalendarsController_getCalendarEventDetails",
|
"operationId": "CalUnifiedCalendarsController_getCalendarEventDetails",
|
||||||
"summary": "Get meeting Details from calendar",
|
"summary": "Get meeting details from calendar",
|
||||||
"description": "Returns detailed information about a meeting including attendance metrics",
|
"description": "Returns detailed information about a meeting including attendance metrics",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -10194,8 +10335,8 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/request": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_requestEmailVerificationCode",
|
"operationId": "OrgTeamsVerifiedResourcesController_requestEmailVerificationCode",
|
||||||
"summary": "Request Email Verification Code",
|
"summary": "Request email verification code",
|
||||||
"description": "Sends a verification code to the email.",
|
"description": "Sends a verification code to the email",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
@@ -10237,8 +10378,8 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/request": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_requestPhoneVerificationCode",
|
"operationId": "OrgTeamsVerifiedResourcesController_requestPhoneVerificationCode",
|
||||||
"summary": "Request Phone Number Verification Code",
|
"summary": "Request phone number verification code",
|
||||||
"description": "Sends a verification code to the phone number.",
|
"description": "Sends a verification code to the phone number",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
@@ -10280,7 +10421,7 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/verify": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_verifyEmail",
|
"operationId": "OrgTeamsVerifiedResourcesController_verifyEmail",
|
||||||
"summary": "Verify an email for an org team.",
|
"summary": "Verify an email for an org team",
|
||||||
"description": "Use code to verify an email",
|
"description": "Use code to verify an email",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -10331,7 +10472,7 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/verify": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_verifyPhoneNumber",
|
"operationId": "OrgTeamsVerifiedResourcesController_verifyPhoneNumber",
|
||||||
"summary": "Verify a phone number for an org team.",
|
"summary": "Verify a phone number for an org team",
|
||||||
"description": "Use code to verify a phone number",
|
"description": "Use code to verify a phone number",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -10382,7 +10523,7 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmails",
|
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmails",
|
||||||
"summary": "Get list of verified emails of an org team.",
|
"summary": "Get list of verified emails of an org team",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "teamId",
|
"name": "teamId",
|
||||||
@@ -10447,7 +10588,7 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneNumbers",
|
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneNumbers",
|
||||||
"summary": "Get list of verified phone numbers of an org team.",
|
"summary": "Get list of verified phone numbers of an org team",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "teamId",
|
"name": "teamId",
|
||||||
@@ -10512,7 +10653,7 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/{id}": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmailById",
|
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmailById",
|
||||||
"summary": "Get verified email of an org team by id.",
|
"summary": "Get verified email of an org team by id",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -10560,7 +10701,7 @@
|
|||||||
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/{id}": {
|
"/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneById",
|
"operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneById",
|
||||||
"summary": "Get verified phone number of an org team by id.",
|
"summary": "Get verified phone number of an org team by id",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "teamId",
|
"name": "teamId",
|
||||||
@@ -10605,147 +10746,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
|
|
||||||
"summary": "Get Stripe connect URL for a team",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"required": true,
|
|
||||||
"in": "header",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "teamId",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "orgId",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "returnTo",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "onErrorReturnTo",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/StripConnectOutputResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Organizations/Teams Stripe"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/stripe/check": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "OrganizationsStripeController_checkTeamStripeConnection",
|
|
||||||
"summary": "Check team Stripe connection",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "teamId",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/StripCredentialsCheckOutputResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Organizations/Teams Stripe"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/stripe/save": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "OrganizationsStripeController_save",
|
|
||||||
"summary": "Save Stripe credentials",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "code",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "teamId",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/StripCredentialsSaveOutputResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Organizations/Teams Stripe"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/v2/routing-forms/{routingFormId}/calculate-slots": {
|
"/v2/routing-forms/{routingFormId}/calculate-slots": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "RoutingFormsController_calculateSlotsBasedOnRoutingFormResponse",
|
"operationId": "RoutingFormsController_calculateSlotsBasedOnRoutingFormResponse",
|
||||||
@@ -12519,8 +12519,8 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/emails/verification-code/request": {
|
"/v2/teams/{teamId}/verified-resources/emails/verification-code/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode",
|
"operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode",
|
||||||
"summary": "Request Email Verification Code",
|
"summary": "Request email verification code",
|
||||||
"description": "Sends a verification code to the Email.",
|
"description": "Sends a verification code to the Email",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
@@ -12562,8 +12562,8 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/phones/verification-code/request": {
|
"/v2/teams/{teamId}/verified-resources/phones/verification-code/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_requestPhoneVerificationCode",
|
"operationId": "TeamsVerifiedResourcesController_requestPhoneVerificationCode",
|
||||||
"summary": "Request Phone Number Verification Code",
|
"summary": "Request phone number verification code",
|
||||||
"description": "Sends a verification code to the phone number.",
|
"description": "Sends a verification code to the phone number",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
@@ -12605,7 +12605,7 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/emails/verification-code/verify": {
|
"/v2/teams/{teamId}/verified-resources/emails/verification-code/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_verifyEmail",
|
"operationId": "TeamsVerifiedResourcesController_verifyEmail",
|
||||||
"summary": "Verify an email for a team.",
|
"summary": "Verify an email for a team",
|
||||||
"description": "Use code to verify an email",
|
"description": "Use code to verify an email",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -12656,7 +12656,7 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/phones/verification-code/verify": {
|
"/v2/teams/{teamId}/verified-resources/phones/verification-code/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_verifyPhoneNumber",
|
"operationId": "TeamsVerifiedResourcesController_verifyPhoneNumber",
|
||||||
"summary": "Verify a phone number for an org team.",
|
"summary": "Verify a phone number for an org team",
|
||||||
"description": "Use code to verify a phone number",
|
"description": "Use code to verify a phone number",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -12707,7 +12707,7 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/emails": {
|
"/v2/teams/{teamId}/verified-resources/emails": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_getVerifiedEmails",
|
"operationId": "TeamsVerifiedResourcesController_getVerifiedEmails",
|
||||||
"summary": "Get list of verified emails of a team.",
|
"summary": "Get list of verified emails of a team",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "teamId",
|
"name": "teamId",
|
||||||
@@ -12772,7 +12772,7 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/phones": {
|
"/v2/teams/{teamId}/verified-resources/phones": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneNumbers",
|
"operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneNumbers",
|
||||||
"summary": "Get list of verified phone numbers of a team.",
|
"summary": "Get list of verified phone numbers of a team",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "teamId",
|
"name": "teamId",
|
||||||
@@ -12837,7 +12837,7 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/emails/{id}": {
|
"/v2/teams/{teamId}/verified-resources/emails/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_getVerifiedEmailById",
|
"operationId": "TeamsVerifiedResourcesController_getVerifiedEmailById",
|
||||||
"summary": "Get verified email of a team by id.",
|
"summary": "Get verified email of a team by id",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -12885,7 +12885,7 @@
|
|||||||
"/v2/teams/{teamId}/verified-resources/phones/{id}": {
|
"/v2/teams/{teamId}/verified-resources/phones/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneById",
|
"operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneById",
|
||||||
"summary": "Get verified phone number of a team by id.",
|
"summary": "Get verified phone number of a team by id",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "teamId",
|
"name": "teamId",
|
||||||
@@ -12933,8 +12933,8 @@
|
|||||||
"/v2/verified-resources/emails/verification-code/request": {
|
"/v2/verified-resources/emails/verification-code/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "UserVerifiedResourcesController_requestEmailVerificationCode",
|
"operationId": "UserVerifiedResourcesController_requestEmailVerificationCode",
|
||||||
"summary": "Request Email Verification Code",
|
"summary": "Request email verification code",
|
||||||
"description": "Sends a verification code to the email.",
|
"description": "Sends a verification code to the email",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
@@ -12976,8 +12976,8 @@
|
|||||||
"/v2/verified-resources/phones/verification-code/request": {
|
"/v2/verified-resources/phones/verification-code/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "UserVerifiedResourcesController_requestPhoneVerificationCode",
|
"operationId": "UserVerifiedResourcesController_requestPhoneVerificationCode",
|
||||||
"summary": "Request Phone Number Verification Code",
|
"summary": "Request phone number verification code",
|
||||||
"description": "Sends a verification code to the phone number.",
|
"description": "Sends a verification code to the phone number",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
@@ -13019,7 +13019,7 @@
|
|||||||
"/v2/verified-resources/emails/verification-code/verify": {
|
"/v2/verified-resources/emails/verification-code/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "UserVerifiedResourcesController_verifyEmail",
|
"operationId": "UserVerifiedResourcesController_verifyEmail",
|
||||||
"summary": "Verify an email.",
|
"summary": "Verify an email",
|
||||||
"description": "Use code to verify an email",
|
"description": "Use code to verify an email",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -13062,7 +13062,7 @@
|
|||||||
"/v2/verified-resources/phones/verification-code/verify": {
|
"/v2/verified-resources/phones/verification-code/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "UserVerifiedResourcesController_verifyPhoneNumber",
|
"operationId": "UserVerifiedResourcesController_verifyPhoneNumber",
|
||||||
"summary": "Verify a phone number.",
|
"summary": "Verify a phone number",
|
||||||
"description": "Use code to verify a phone number",
|
"description": "Use code to verify a phone number",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -13105,7 +13105,7 @@
|
|||||||
"/v2/verified-resources/emails": {
|
"/v2/verified-resources/emails": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "UserVerifiedResourcesController_getVerifiedEmails",
|
"operationId": "UserVerifiedResourcesController_getVerifiedEmails",
|
||||||
"summary": "Get list of verified emails.",
|
"summary": "Get list of verified emails",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "take",
|
"name": "take",
|
||||||
@@ -13162,7 +13162,7 @@
|
|||||||
"/v2/verified-resources/phones": {
|
"/v2/verified-resources/phones": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "UserVerifiedResourcesController_getVerifiedPhoneNumbers",
|
"operationId": "UserVerifiedResourcesController_getVerifiedPhoneNumbers",
|
||||||
"summary": "Get list of verified phone numbers.",
|
"summary": "Get list of verified phone numbers",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "take",
|
"name": "take",
|
||||||
@@ -13219,7 +13219,7 @@
|
|||||||
"/v2/verified-resources/emails/{id}": {
|
"/v2/verified-resources/emails/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "UserVerifiedResourcesController_getVerifiedEmailById",
|
"operationId": "UserVerifiedResourcesController_getVerifiedEmailById",
|
||||||
"summary": "Get verified email by id.",
|
"summary": "Get verified email by id",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -13259,7 +13259,7 @@
|
|||||||
"/v2/verified-resources/phones/{id}": {
|
"/v2/verified-resources/phones/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "UserVerifiedResourcesController_getVerifiedPhoneById",
|
"operationId": "UserVerifiedResourcesController_getVerifiedPhoneById",
|
||||||
"summary": "Get verified phone number by id.",
|
"summary": "Get verified phone number by id",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -13614,7 +13614,8 @@
|
|||||||
"sk",
|
"sk",
|
||||||
"ta",
|
"ta",
|
||||||
"uk",
|
"uk",
|
||||||
"zh-TW"
|
"zh-TW",
|
||||||
|
"bn"
|
||||||
],
|
],
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "en"
|
"example": "en"
|
||||||
@@ -13750,7 +13751,8 @@
|
|||||||
"sk",
|
"sk",
|
||||||
"ta",
|
"ta",
|
||||||
"uk",
|
"uk",
|
||||||
"zh-TW"
|
"zh-TW",
|
||||||
|
"bn"
|
||||||
],
|
],
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "en"
|
"example": "en"
|
||||||
@@ -13930,7 +13932,8 @@
|
|||||||
"sk",
|
"sk",
|
||||||
"ta",
|
"ta",
|
||||||
"uk",
|
"uk",
|
||||||
"zh-TW"
|
"zh-TW",
|
||||||
|
"bn"
|
||||||
],
|
],
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "en"
|
"example": "en"
|
||||||
@@ -23972,7 +23975,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {}
|
||||||
},
|
},
|
||||||
"Attendee": {
|
"CreateBookingAttendee": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -24039,7 +24042,8 @@
|
|||||||
"sk",
|
"sk",
|
||||||
"ta",
|
"ta",
|
||||||
"uk",
|
"uk",
|
||||||
"zh-TW"
|
"zh-TW",
|
||||||
|
"bn"
|
||||||
],
|
],
|
||||||
"description": "The preferred language of the attendee. Used for booking confirmation.",
|
"description": "The preferred language of the attendee. Used for booking confirmation.",
|
||||||
"example": "it",
|
"example": "it",
|
||||||
@@ -24108,7 +24112,7 @@
|
|||||||
"description": "The attendee's details.",
|
"description": "The attendee's details.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Attendee"
|
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -24235,7 +24239,7 @@
|
|||||||
"description": "The attendee's details.",
|
"description": "The attendee's details.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Attendee"
|
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -24368,7 +24372,7 @@
|
|||||||
"description": "The attendee's details.",
|
"description": "The attendee's details.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Attendee"
|
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -24537,6 +24541,86 @@
|
|||||||
"slug"
|
"slug"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"BookingAttendee": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "John Doe"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john@example.com"
|
||||||
|
},
|
||||||
|
"timeZone": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "America/New_York"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ar",
|
||||||
|
"ca",
|
||||||
|
"de",
|
||||||
|
"es",
|
||||||
|
"eu",
|
||||||
|
"he",
|
||||||
|
"id",
|
||||||
|
"ja",
|
||||||
|
"lv",
|
||||||
|
"pl",
|
||||||
|
"ro",
|
||||||
|
"sr",
|
||||||
|
"th",
|
||||||
|
"vi",
|
||||||
|
"az",
|
||||||
|
"cs",
|
||||||
|
"el",
|
||||||
|
"es-419",
|
||||||
|
"fi",
|
||||||
|
"hr",
|
||||||
|
"it",
|
||||||
|
"km",
|
||||||
|
"nl",
|
||||||
|
"pt",
|
||||||
|
"ru",
|
||||||
|
"sv",
|
||||||
|
"tr",
|
||||||
|
"zh-CN",
|
||||||
|
"bg",
|
||||||
|
"da",
|
||||||
|
"en",
|
||||||
|
"et",
|
||||||
|
"fr",
|
||||||
|
"hu",
|
||||||
|
"iw",
|
||||||
|
"ko",
|
||||||
|
"no",
|
||||||
|
"pt-BR",
|
||||||
|
"sk",
|
||||||
|
"ta",
|
||||||
|
"uk",
|
||||||
|
"zh-TW",
|
||||||
|
"bn"
|
||||||
|
],
|
||||||
|
"example": "en"
|
||||||
|
},
|
||||||
|
"absent": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"phoneNumber": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "+1234567890"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"email",
|
||||||
|
"timeZone",
|
||||||
|
"absent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"BookingOutput_2024_08_13": {
|
"BookingOutput_2024_08_13": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -24590,7 +24674,13 @@
|
|||||||
},
|
},
|
||||||
"rescheduledFromUid": {
|
"rescheduledFromUid": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "previous_uid_123"
|
"example": "previous_uid_123",
|
||||||
|
"description": "UID of the previous booking from which this booking was rescheduled."
|
||||||
|
},
|
||||||
|
"rescheduledToUid": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "new_uid_456",
|
||||||
|
"description": "UID of the new booking to which this booking was rescheduled."
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -24653,7 +24743,7 @@
|
|||||||
"attendees": {
|
"attendees": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/Attendee"
|
"$ref": "#/components/schemas/BookingAttendee"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"guests": {
|
"guests": {
|
||||||
@@ -24747,7 +24837,13 @@
|
|||||||
},
|
},
|
||||||
"rescheduledFromUid": {
|
"rescheduledFromUid": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "previous_uid_123"
|
"example": "previous_uid_123",
|
||||||
|
"description": "UID of the previous booking from which this booking was rescheduled."
|
||||||
|
},
|
||||||
|
"rescheduledToUid": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "new_uid_456",
|
||||||
|
"description": "UID of the new booking to which this booking was rescheduled."
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -24810,7 +24906,7 @@
|
|||||||
"attendees": {
|
"attendees": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/Attendee"
|
"$ref": "#/components/schemas/BookingAttendee"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"guests": {
|
"guests": {
|
||||||
@@ -24915,7 +25011,8 @@
|
|||||||
"sk",
|
"sk",
|
||||||
"ta",
|
"ta",
|
||||||
"uk",
|
"uk",
|
||||||
"zh-TW"
|
"zh-TW",
|
||||||
|
"bn"
|
||||||
],
|
],
|
||||||
"example": "en"
|
"example": "en"
|
||||||
},
|
},
|
||||||
@@ -25007,7 +25104,13 @@
|
|||||||
},
|
},
|
||||||
"rescheduledFromUid": {
|
"rescheduledFromUid": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "previous_uid_123"
|
"example": "previous_uid_123",
|
||||||
|
"description": "UID of the previous booking from which this booking was rescheduled."
|
||||||
|
},
|
||||||
|
"rescheduledToUid": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "new_uid_456",
|
||||||
|
"description": "UID of the new booking to which this booking was rescheduled."
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -25151,7 +25254,13 @@
|
|||||||
},
|
},
|
||||||
"rescheduledFromUid": {
|
"rescheduledFromUid": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "previous_uid_123"
|
"example": "previous_uid_123",
|
||||||
|
"description": "UID of the previous booking from which this booking was rescheduled."
|
||||||
|
},
|
||||||
|
"rescheduledToUid": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "new_uid_456",
|
||||||
|
"description": "UID of the new booking to which this booking was rescheduled."
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -25340,7 +25449,13 @@
|
|||||||
},
|
},
|
||||||
"rescheduledFromUid": {
|
"rescheduledFromUid": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "previous_uid_123"
|
"example": "previous_uid_123",
|
||||||
|
"description": "UID of the previous booking from which this booking was rescheduled."
|
||||||
|
},
|
||||||
|
"rescheduledToUid": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "new_uid_456",
|
||||||
|
"description": "UID of the new booking to which this booking was rescheduled."
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -25479,7 +25594,13 @@
|
|||||||
},
|
},
|
||||||
"rescheduledFromUid": {
|
"rescheduledFromUid": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "previous_uid_123"
|
"example": "previous_uid_123",
|
||||||
|
"description": "UID of the previous booking from which this booking was rescheduled."
|
||||||
|
},
|
||||||
|
"rescheduledToUid": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "new_uid_456",
|
||||||
|
"description": "UID of the new booking to which this booking was rescheduled."
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -25921,6 +26042,21 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"MarkAbsentAttendee": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"absent": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"absent"
|
||||||
|
]
|
||||||
|
},
|
||||||
"MarkAbsentBookingInput_2024_08_13": {
|
"MarkAbsentBookingInput_2024_08_13": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -25932,7 +26068,7 @@
|
|||||||
"attendees": {
|
"attendees": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/Attendee"
|
"$ref": "#/components/schemas/MarkAbsentAttendee"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export class CalendarsServiceMock {
|
|||||||
id: 0,
|
id: 0,
|
||||||
delegationCredentialId: null,
|
delegationCredentialId: null,
|
||||||
domainWideDelegationCredentialId: null,
|
domainWideDelegationCredentialId: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
} satisfies Awaited<ReturnType<typeof CalendarsService.prototype.getCalendars>>;
|
} satisfies Awaited<ReturnType<typeof CalendarsService.prototype.getCalendars>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"],
|
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"],
|
||||||
"@calcom/platform-libraries/app-store": ["../../../packages/platform/libraries/app-store.ts"],
|
"@calcom/platform-libraries/app-store": ["../../../packages/platform/libraries/app-store.ts"],
|
||||||
"@calcom/platform-libraries/workflows": ["../../../packages/platform/libraries/workflows.ts"],
|
"@calcom/platform-libraries/workflows": ["../../../packages/platform/libraries/workflows.ts"],
|
||||||
"@calcom/platform-libraries/conferencing": ["../../../packages/platform/libraries/conferencing.ts"]
|
"@calcom/platform-libraries/conferencing": ["../../../packages/platform/libraries/conferencing.ts"],
|
||||||
|
"@calcom/platform-libraries/repositories": ["../../../packages/platform/libraries/repositories.ts"]
|
||||||
},
|
},
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
@@ -70,9 +70,7 @@ export const ComplexExample: React.FC = () => (
|
|||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="cursor-pointer">
|
<Button variant="icon" color="secondary" StartIcon="calendar" />
|
||||||
<Button variant="icon" color="secondary" StartIcon="calendar" />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownItem>Rename</DropdownItem>
|
<DropdownItem>Rename</DropdownItem>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"eslint": "^9",
|
"eslint": "^8.34.0",
|
||||||
"eslint-config-next": "15.1.6",
|
"eslint-config-next": "15.1.6",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ import { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps
|
|||||||
|
|
||||||
import TypePage, { type PageProps as ClientPageProps } from "~/users/views/users-type-public-view";
|
import TypePage, { type PageProps as ClientPageProps } from "~/users/views/users-type-public-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return {
|
||||||
|
robots: {
|
||||||
|
follow: false,
|
||||||
|
index: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
|||||||
|
|
||||||
import User, { type PageProps as ClientPageProps } from "~/users/views/users-public-view";
|
import User, { type PageProps as ClientPageProps } from "~/users/views/users-public-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return {
|
||||||
|
robots: {
|
||||||
|
follow: false,
|
||||||
|
index: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
|||||||
|
|
||||||
export type ClientPageProps = UserTypePageProps | TeamTypePageProps;
|
export type ClientPageProps = UserTypePageProps | TeamTypePageProps;
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return {
|
||||||
|
robots: {
|
||||||
|
follow: false,
|
||||||
|
index: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||||
const props = await getData(context);
|
const props = await getData(context);
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
|||||||
|
|
||||||
export type ClientPageProps = UserPageProps | TeamPageProps;
|
export type ClientPageProps = UserPageProps | TeamPageProps;
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return {
|
||||||
|
robots: {
|
||||||
|
follow: false,
|
||||||
|
index: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||||
const props = await getData(context);
|
const props = await getData(context);
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
|||||||
|
|
||||||
import TypePage, { type PageProps as ClientPageProps } from "~/team/type-view";
|
import TypePage, { type PageProps as ClientPageProps } from "~/team/type-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return {
|
||||||
|
robots: {
|
||||||
|
follow: false,
|
||||||
|
index: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
|||||||
|
|
||||||
import TeamPage, { type PageProps as ClientPageProps } from "~/team/team-view";
|
import TeamPage, { type PageProps as ClientPageProps } from "~/team/team-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return {
|
||||||
|
robots: {
|
||||||
|
follow: false,
|
||||||
|
index: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { unstable_cache } from "next/cache";
|
|||||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
import { TeamService } from "@calcom/lib/server/service/team";
|
import { TeamService } from "@calcom/lib/server/service/team";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -14,7 +15,8 @@ import { TeamsCTA } from "./CTA";
|
|||||||
|
|
||||||
const getCachedTeams = unstable_cache(
|
const getCachedTeams = unstable_cache(
|
||||||
async (userId: number) => {
|
async (userId: number) => {
|
||||||
return await TeamRepository.findTeamsByUserId({
|
const teamRepo = new TeamRepository(prisma);
|
||||||
|
return await teamRepo.findTeamsByUserId({
|
||||||
userId,
|
userId,
|
||||||
includeOrgs: true,
|
includeOrgs: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { cookies, headers } from "next/headers";
|
|||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import type { AppCategories } from "@calcom/prisma/enums";
|
import type { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
@@ -25,7 +26,8 @@ const ServerPage = async () => {
|
|||||||
const session = await getServerSession({ req });
|
const session = await getServerSession({ req });
|
||||||
let appStore, userAdminTeamsIds: number[];
|
let appStore, userAdminTeamsIds: number[];
|
||||||
if (session?.user?.id) {
|
if (session?.user?.id) {
|
||||||
const userAdminTeams = await UserRepository.getUserAdminTeams(session.user.id);
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const userAdminTeams = await userRepo.getUserAdminTeams({ userId: session.user.id });
|
||||||
userAdminTeamsIds = userAdminTeams?.teams?.map(({ team }) => team.id) ?? [];
|
userAdminTeamsIds = userAdminTeams?.teams?.map(({ team }) => team.id) ?? [];
|
||||||
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeamsIds);
|
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeamsIds);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+5
-2
@@ -6,6 +6,7 @@ import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequir
|
|||||||
import { UsersEditView } from "@calcom/features/ee/users/pages/users-edit-view";
|
import { UsersEditView } from "@calcom/features/ee/users/pages/users-edit-view";
|
||||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
const userIdSchema = z.object({ id: z.coerce.number() });
|
const userIdSchema = z.object({ id: z.coerce.number() });
|
||||||
|
|
||||||
@@ -21,7 +22,8 @@ export const generateMetadata = async ({ params }: { params: Params }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await UserRepository.adminFindById(input.data.id);
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const user = await userRepo.adminFindById(input.data.id);
|
||||||
|
|
||||||
return await _generateMetadata(
|
return await _generateMetadata(
|
||||||
(t) => `${t("editing_user")}: ${user.username}`,
|
(t) => `${t("editing_user")}: ${user.username}`,
|
||||||
@@ -37,7 +39,8 @@ const Page = async ({ params }: { params: Params }) => {
|
|||||||
|
|
||||||
if (!input.success) throw new Error("Invalid access");
|
if (!input.success) throw new Error("Invalid access");
|
||||||
|
|
||||||
const user = await UserRepository.adminFindById(input.data.id);
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const user = await userRepo.adminFindById(input.data.id);
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+47
-11
@@ -10,6 +10,8 @@ import React, { useEffect, useState, useMemo } from "react";
|
|||||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||||
import type { OrganizationBranding } from "@calcom/features/ee/organizations/context/provider";
|
import type { OrganizationBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||||
|
import type { TeamFeatures } from "@calcom/features/flags/config";
|
||||||
|
import { useIsFeatureEnabledForTeam } from "@calcom/features/flags/hooks/useIsFeatureEnabledForTeam";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
@@ -177,7 +179,13 @@ const organizationAdminKeys = [
|
|||||||
"delegation_credential",
|
"delegation_credential",
|
||||||
];
|
];
|
||||||
|
|
||||||
const useTabs = ({ isDelegationCredentialEnabled }: { isDelegationCredentialEnabled: boolean }) => {
|
const useTabs = ({
|
||||||
|
isDelegationCredentialEnabled,
|
||||||
|
isPbacEnabled,
|
||||||
|
}: {
|
||||||
|
isDelegationCredentialEnabled: boolean;
|
||||||
|
isPbacEnabled: boolean;
|
||||||
|
}) => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
@@ -213,6 +221,14 @@ const useTabs = ({ isDelegationCredentialEnabled }: { isDelegationCredentialEnab
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add pbac menu item only if feature flag is enabled
|
||||||
|
if (isPbacEnabled) {
|
||||||
|
newArray.push({
|
||||||
|
name: "roles_and_permissions",
|
||||||
|
href: "/settings/organizations/roles",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...tab,
|
...tab,
|
||||||
children: newArray,
|
children: newArray,
|
||||||
@@ -272,6 +288,7 @@ interface SettingsSidebarContainerProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
navigationIsOpenedOnMobile?: boolean;
|
navigationIsOpenedOnMobile?: boolean;
|
||||||
bannersHeight?: number;
|
bannersHeight?: number;
|
||||||
|
teamFeatures?: Record<number, TeamFeatures>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TeamListCollapsible = () => {
|
const TeamListCollapsible = () => {
|
||||||
@@ -438,6 +455,7 @@ const SettingsSidebarContainer = ({
|
|||||||
className = "",
|
className = "",
|
||||||
navigationIsOpenedOnMobile,
|
navigationIsOpenedOnMobile,
|
||||||
bannersHeight,
|
bannersHeight,
|
||||||
|
teamFeatures,
|
||||||
}: SettingsSidebarContainerProps) => {
|
}: SettingsSidebarContainerProps) => {
|
||||||
const searchParams = useCompatSearchParams();
|
const searchParams = useCompatSearchParams();
|
||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
@@ -449,11 +467,24 @@ const SettingsSidebarContainer = ({
|
|||||||
}[]
|
}[]
|
||||||
>();
|
>();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const { data } = trpc.viewer.delegationCredential.check.useQuery(undefined, {
|
|
||||||
enabled: !!session.data?.user?.org,
|
const organizationId = session.data?.user?.org?.id;
|
||||||
|
|
||||||
|
const isDelegationCredentialEnabled = useIsFeatureEnabledForTeam({
|
||||||
|
teamFeatures,
|
||||||
|
teamId: organizationId,
|
||||||
|
feature: "delegation-credential",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isPbacEnabled = useIsFeatureEnabledForTeam({
|
||||||
|
teamFeatures,
|
||||||
|
teamId: organizationId,
|
||||||
|
feature: "pbac",
|
||||||
|
});
|
||||||
|
|
||||||
const tabsWithPermissions = useTabs({
|
const tabsWithPermissions = useTabs({
|
||||||
isDelegationCredentialEnabled: data?.hasDelegationCredential ?? false,
|
isDelegationCredentialEnabled,
|
||||||
|
isPbacEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, {
|
const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, {
|
||||||
@@ -727,9 +758,10 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
|
|||||||
export type SettingsLayoutProps = {
|
export type SettingsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
teamFeatures?: Record<number, TeamFeatures>;
|
||||||
} & ComponentProps<typeof Shell>;
|
} & ComponentProps<typeof Shell>;
|
||||||
|
|
||||||
export default function SettingsLayoutAppDirClient({ children, ...rest }: SettingsLayoutProps) {
|
export default function SettingsLayoutAppDirClient({ children, teamFeatures, ...rest }: SettingsLayoutProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const state = useState(false);
|
const state = useState(false);
|
||||||
const [sideContainerOpen, setSideContainerOpen] = state;
|
const [sideContainerOpen, setSideContainerOpen] = state;
|
||||||
@@ -761,6 +793,7 @@ export default function SettingsLayoutAppDirClient({ children, ...rest }: Settin
|
|||||||
<SidebarContainerElement
|
<SidebarContainerElement
|
||||||
sideContainerOpen={sideContainerOpen}
|
sideContainerOpen={sideContainerOpen}
|
||||||
setSideContainerOpen={setSideContainerOpen}
|
setSideContainerOpen={setSideContainerOpen}
|
||||||
|
teamFeatures={teamFeatures}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
drawerState={state}
|
drawerState={state}
|
||||||
@@ -778,10 +811,18 @@ export default function SettingsLayoutAppDirClient({ children, ...rest }: Settin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SidebarContainerElementProps = {
|
||||||
|
sideContainerOpen: boolean;
|
||||||
|
bannersHeight?: number;
|
||||||
|
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
teamFeatures?: Record<number, TeamFeatures>;
|
||||||
|
};
|
||||||
|
|
||||||
const SidebarContainerElement = ({
|
const SidebarContainerElement = ({
|
||||||
sideContainerOpen,
|
sideContainerOpen,
|
||||||
bannersHeight,
|
bannersHeight,
|
||||||
setSideContainerOpen,
|
setSideContainerOpen,
|
||||||
|
teamFeatures,
|
||||||
}: SidebarContainerElementProps) => {
|
}: SidebarContainerElementProps) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
@@ -797,13 +838,8 @@ const SidebarContainerElement = ({
|
|||||||
<SettingsSidebarContainer
|
<SettingsSidebarContainer
|
||||||
navigationIsOpenedOnMobile={sideContainerOpen}
|
navigationIsOpenedOnMobile={sideContainerOpen}
|
||||||
bannersHeight={bannersHeight}
|
bannersHeight={bannersHeight}
|
||||||
|
teamFeatures={teamFeatures}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type SidebarContainerElementProps = {
|
|
||||||
sideContainerOpen: boolean;
|
|
||||||
bannersHeight?: number;
|
|
||||||
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
|
import { unstable_cache } from "next/cache";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import type { TeamFeatures } from "@calcom/features/flags/config";
|
||||||
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
|
|
||||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import type { SettingsLayoutProps } from "./SettingsLayoutAppDirClient";
|
import type { SettingsLayoutProps } from "./SettingsLayoutAppDirClient";
|
||||||
import SettingsLayoutAppDirClient from "./SettingsLayoutAppDirClient";
|
import SettingsLayoutAppDirClient from "./SettingsLayoutAppDirClient";
|
||||||
|
|
||||||
|
const getTeamFeatures = unstable_cache(
|
||||||
|
async (teamId: number) => {
|
||||||
|
const featuresRepository = new FeaturesRepository();
|
||||||
|
return await featuresRepository.getTeamFeatures(teamId);
|
||||||
|
},
|
||||||
|
["team-features"],
|
||||||
|
{
|
||||||
|
revalidate: 120,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
||||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
const userId = session?.user?.id;
|
const userId = session?.user?.id;
|
||||||
@@ -16,9 +30,21 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
|||||||
return redirect("/auth/login");
|
return redirect("/auth/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let teamFeatures: Record<number, TeamFeatures> | null = null;
|
||||||
|
const orgId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
|
||||||
|
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
|
||||||
|
if (orgId) {
|
||||||
|
const features = await getTeamFeatures(orgId);
|
||||||
|
if (features) {
|
||||||
|
teamFeatures = {
|
||||||
|
[orgId]: features,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsLayoutAppDirClient {...props} />
|
<SettingsLayoutAppDirClient {...props} teamFeatures={teamFeatures ?? {}} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { PERMISSION_REGISTRY, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
import { Icon } from "@calcom/ui/components/icon";
|
||||||
|
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||||
|
import { Checkbox, Label } from "@calcom/ui/form";
|
||||||
|
|
||||||
|
import { usePermissions } from "./usePermissions";
|
||||||
|
|
||||||
|
interface AdvancedPermissionGroupProps {
|
||||||
|
resource: Resource;
|
||||||
|
selectedPermissions: string[];
|
||||||
|
onChange: (permissions: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INTERNAL_DATAACCESS_KEY = "_resource";
|
||||||
|
|
||||||
|
export function AdvancedPermissionGroup({
|
||||||
|
resource,
|
||||||
|
selectedPermissions,
|
||||||
|
onChange,
|
||||||
|
}: AdvancedPermissionGroupProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { toggleSinglePermission, toggleResourcePermissionLevel } = usePermissions();
|
||||||
|
const resourceConfig = PERMISSION_REGISTRY[resource];
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const isAllResources = resource === "*";
|
||||||
|
const allResourcesSelected = selectedPermissions.includes("*.*");
|
||||||
|
|
||||||
|
// Get all possible permissions for this resource
|
||||||
|
const allPermissions = isAllResources
|
||||||
|
? ["*.*"]
|
||||||
|
: Object.entries(resourceConfig)
|
||||||
|
.filter(([action]) => action !== INTERNAL_DATAACCESS_KEY)
|
||||||
|
.map(([action]) => `${resource}.${action}`);
|
||||||
|
|
||||||
|
// Check if all permissions for this resource are selected
|
||||||
|
const isAllSelected = isAllResources
|
||||||
|
? allResourcesSelected
|
||||||
|
: allPermissions.every((p) => selectedPermissions.includes(p));
|
||||||
|
|
||||||
|
const handleToggleAll = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation(); // Stop event from triggering parent click
|
||||||
|
onChange(toggleResourcePermissionLevel(resource, isAllSelected ? "none" : "all", selectedPermissions));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to check if read permission is auto-enabled
|
||||||
|
const isReadAutoEnabled = (action: string) => {
|
||||||
|
if (action === CrudAction.Read) return false;
|
||||||
|
if (action === CrudAction.Create || action === CrudAction.Update || action === CrudAction.Delete) {
|
||||||
|
return selectedPermissions.includes(`${resource}.${CrudAction.Read}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-muted border-subtle mb-2 rounded-xl border">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex cursor-pointer items-center justify-between gap-1.5 p-4"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}>
|
||||||
|
<Icon
|
||||||
|
name={isAllResources ? "chevron-right" : "chevron-down"}
|
||||||
|
className={classNames(
|
||||||
|
"h-4 w-4 transition-transform",
|
||||||
|
isExpanded && !isAllResources ? "rotate-180" : ""
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={isAllSelected}
|
||||||
|
onCheckedChange={() => handleToggleAll}
|
||||||
|
onClick={handleToggleAll}
|
||||||
|
/>
|
||||||
|
<span className="text-default text-sm font-medium leading-none">
|
||||||
|
{t(resourceConfig._resource?.i18nKey || "")}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted text-sm font-medium leading-none">{t("all_permissions")}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{isExpanded && !isAllResources && (
|
||||||
|
<div
|
||||||
|
className="bg-default border-muted m-1 flex flex-col gap-2.5 rounded-xl border p-3"
|
||||||
|
onClick={(e) => e.stopPropagation()} // Stop clicks in the permission list from affecting parent
|
||||||
|
>
|
||||||
|
{Object.entries(resourceConfig).map(([action, actionConfig]) => {
|
||||||
|
const permission = `${resource}.${action}`;
|
||||||
|
|
||||||
|
if (action === INTERNAL_DATAACCESS_KEY) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChecked = selectedPermissions.includes(permission);
|
||||||
|
const isReadPermission = action === CrudAction.Read;
|
||||||
|
const isAutoEnabled = isReadAutoEnabled(action);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={action} className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id={permission}
|
||||||
|
checked={isChecked}
|
||||||
|
className="mr-2"
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onChange(toggleSinglePermission(permission, !!checked, selectedPermissions));
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()} // Stop checkbox clicks from affecting parent
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.stopPropagation()} // Stop label clicks from affecting parent
|
||||||
|
>
|
||||||
|
<Label htmlFor={permission} className="mb-0">
|
||||||
|
<span className={classNames(isAutoEnabled && "text-muted-foreground")}>
|
||||||
|
{t(actionConfig?.i18nKey || "")}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{t(
|
||||||
|
actionConfig && "descriptionI18nKey" in actionConfig
|
||||||
|
? actionConfig.descriptionI18nKey
|
||||||
|
: ""
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{isAutoEnabled && (
|
||||||
|
<Tooltip content={t("read_permission_auto_enabled_tooltip")}>
|
||||||
|
<Icon name="info" className="text-muted-foreground h-3 w-3" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}{" "}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button } from "@calcom/ui/button";
|
||||||
|
|
||||||
|
import { useRoleSheetState } from "../hooks/useRoleQueryStates";
|
||||||
|
|
||||||
|
export function CreateRoleCTA() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { setIsOpen } = useRoleSheetState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button StartIcon="plus" onClick={() => setIsOpen(true)}>
|
||||||
|
{t("create_role")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Dialog } from "@calcom/ui/components/dialog";
|
||||||
|
import { ConfirmationDialogContent } from "@calcom/ui/components/dialog";
|
||||||
|
import { DropdownItem } from "@calcom/ui/components/dropdown";
|
||||||
|
import { showToast } from "@calcom/ui/components/toast";
|
||||||
|
|
||||||
|
import { revalidateTeamRoles } from "../actions";
|
||||||
|
|
||||||
|
interface DeleteRoleModalProps {
|
||||||
|
roleId: string;
|
||||||
|
roleName: string;
|
||||||
|
teamId: number;
|
||||||
|
onDeleted?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteRoleModal({ roleId, roleName, teamId, onDeleted }: DeleteRoleModalProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const deleteMutation = trpc.viewer.pbac.deleteRole.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(t("role_deleted_successfully"), "success");
|
||||||
|
setIsModalOpen(false);
|
||||||
|
await revalidateTeamRoles();
|
||||||
|
router.refresh();
|
||||||
|
onDeleted?.();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message || t("error_deleting_role"), "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
|
<DropdownItem StartIcon="trash" color="destructive" onClick={() => setIsModalOpen(true)}>
|
||||||
|
{t("delete")}
|
||||||
|
</DropdownItem>
|
||||||
|
<ConfirmationDialogContent
|
||||||
|
variety="danger"
|
||||||
|
title={t("delete_role")}
|
||||||
|
confirmBtnText={t("delete")}
|
||||||
|
cancelBtnText={t("cancel")}
|
||||||
|
onConfirm={() => {
|
||||||
|
deleteMutation.mutate({
|
||||||
|
teamId,
|
||||||
|
roleId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
loadingText={t("deleting_role")}>
|
||||||
|
<p className="mb-4">
|
||||||
|
{t("confirm_delete_role", {
|
||||||
|
roleName,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
export interface RoleColorPickerProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (color: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RoleColorPicker({ value, onChange }: RoleColorPickerProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [, setCustomColor] = useState(value);
|
||||||
|
const colorInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleCustomColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newColor = e.target.value;
|
||||||
|
setCustomColor(newColor);
|
||||||
|
onChange(newColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColorSquareClick = () => {
|
||||||
|
colorInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleColorSquareClick}
|
||||||
|
className="bg-default text-default border-default hover:bg-muted hover:text-emphasis focus:bg-subtle focus-visible:shadow-outline-gray-focused shadow-outline-gray-rested enabled:hover:shadow-outline-gray-hover enabled:active:shadow-outline-gray-active flex h-8 w-8 items-center justify-center gap-2 rounded-[10px] border text-sm transition-shadow duration-200 focus-visible:outline-none focus-visible:ring-0">
|
||||||
|
<div
|
||||||
|
className="h-5 w-5 rounded border shadow-sm"
|
||||||
|
style={{ backgroundColor: value }}
|
||||||
|
aria-label={t("select_color")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={colorInputRef}
|
||||||
|
type="color"
|
||||||
|
value={value}
|
||||||
|
onChange={handleCustomColorChange}
|
||||||
|
className="sr-only"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState, useMemo, useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { PERMISSION_REGISTRY, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button } from "@calcom/ui/button";
|
||||||
|
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@calcom/ui/components/sheet";
|
||||||
|
import { showToast } from "@calcom/ui/components/toast";
|
||||||
|
import { Form, TextField, Checkbox, Label } from "@calcom/ui/form";
|
||||||
|
|
||||||
|
import { revalidateTeamRoles } from "../actions";
|
||||||
|
import { AdvancedPermissionGroup } from "./AdvancedPermissionGroup";
|
||||||
|
import RoleColorPicker from "./RoleColorPicker";
|
||||||
|
import { SimplePermissionItem } from "./SimplePermissionItem";
|
||||||
|
|
||||||
|
type Role = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
teamId?: number;
|
||||||
|
color?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
type: "SYSTEM" | "CUSTOM";
|
||||||
|
permissions: {
|
||||||
|
id: string;
|
||||||
|
resource: string;
|
||||||
|
action: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: "role_name_required" }),
|
||||||
|
description: z.string().optional(),
|
||||||
|
isAdvancedMode: z.boolean().default(false),
|
||||||
|
permissions: z.array(z.string()),
|
||||||
|
color: z.string().default("#FF5733"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
interface RoleSheetProps {
|
||||||
|
role?: Role;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
teamId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoleSheet({ role, open, onOpenChange, teamId }: RoleSheetProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const isEditing = Boolean(role);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const defaultValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
name: role?.name || "",
|
||||||
|
description: role?.description || "",
|
||||||
|
color: role?.color || "#FF5733",
|
||||||
|
isAdvancedMode: false,
|
||||||
|
permissions: role?.permissions?.map((p) => `${p.resource}.${p.action}`) || [],
|
||||||
|
}),
|
||||||
|
[role]
|
||||||
|
); // Memoize default values based on role
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update form values when role changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Only reset if the sheet is open to prevent unnecessary resets
|
||||||
|
if (open) {
|
||||||
|
if (role) {
|
||||||
|
form.reset({
|
||||||
|
name: role.name,
|
||||||
|
description: role.description || "",
|
||||||
|
color: role.color || "#FF5733",
|
||||||
|
isAdvancedMode: form.getValues("isAdvancedMode"), // Preserve the current mode
|
||||||
|
permissions: role.permissions.map((p) => `${p.resource}.${p.action}`),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
color: "#FF5733",
|
||||||
|
isAdvancedMode: false,
|
||||||
|
permissions: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [role, form, open]);
|
||||||
|
|
||||||
|
const { isAdvancedMode, permissions, color } = form.watch();
|
||||||
|
|
||||||
|
const filteredResources = useMemo(() => {
|
||||||
|
return Object.keys(PERMISSION_REGISTRY).filter((resource) =>
|
||||||
|
t(
|
||||||
|
PERMISSION_REGISTRY[resource as Resource][
|
||||||
|
CrudAction.All as keyof (typeof PERMISSION_REGISTRY)[Resource]
|
||||||
|
]?.i18nKey || ""
|
||||||
|
)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [searchQuery, t]);
|
||||||
|
|
||||||
|
const createMutation = trpc.viewer.pbac.createRole.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(t("role_created_successfully"), "success");
|
||||||
|
form.reset();
|
||||||
|
onOpenChange(false);
|
||||||
|
await revalidateTeamRoles();
|
||||||
|
router.refresh();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message || t("error_creating_role"), "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = trpc.viewer.pbac.updateRole.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(t("role_updated_successfully"), "success");
|
||||||
|
onOpenChange(false);
|
||||||
|
await revalidateTeamRoles();
|
||||||
|
router.refresh();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message || t("error_updating_role"), "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (values: FormValues) => {
|
||||||
|
// Store the color in localStorage
|
||||||
|
const roleKey = isEditing && role ? role.id : `new_role_${values.name}`;
|
||||||
|
localStorage.setItem(`role_color_${roleKey}`, values.color);
|
||||||
|
|
||||||
|
if (isEditing && role) {
|
||||||
|
updateMutation.mutate({
|
||||||
|
teamId,
|
||||||
|
roleId: role.id,
|
||||||
|
name: values.name,
|
||||||
|
permissions: values.permissions as any,
|
||||||
|
color: values.color,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createMutation.mutate({
|
||||||
|
teamId,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
color: values.color,
|
||||||
|
permissions: values.permissions as any,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>{isEditing ? t("edit_role") : t("create_role")}</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<Form form={form} handleSubmit={onSubmit}>
|
||||||
|
<div className="space-y-4 py-5">
|
||||||
|
<div className="flex items-end justify-end gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<TextField
|
||||||
|
label={t("role_name")}
|
||||||
|
{...form.register("name")}
|
||||||
|
placeholder={t("role_name_placeholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<RoleColorPicker
|
||||||
|
value={color}
|
||||||
|
onChange={(value) => form.setValue("color", value, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="">
|
||||||
|
{isAdvancedMode ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<Label className="mb-0">{t("permissions")}</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={form.watch("isAdvancedMode")}
|
||||||
|
onCheckedChange={(checked: boolean) => form.setValue("isAdvancedMode", checked)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{t("advanced")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TextField
|
||||||
|
id="permissions_search"
|
||||||
|
placeholder={t("search_permissions")}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredResources.map((resource) => (
|
||||||
|
<AdvancedPermissionGroup
|
||||||
|
key={resource}
|
||||||
|
resource={resource as Resource}
|
||||||
|
selectedPermissions={permissions}
|
||||||
|
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-muted rounded-xl p-1">
|
||||||
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
|
<Label>{t("permissions")}</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={form.watch("isAdvancedMode")}
|
||||||
|
onCheckedChange={(checked: boolean) => form.setValue("isAdvancedMode", checked)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{t("advanced")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-default border-subtle divide-subtle divide-y rounded-[10px] border">
|
||||||
|
{Object.keys(PERMISSION_REGISTRY).map((resource) => (
|
||||||
|
<SimplePermissionItem
|
||||||
|
key={resource}
|
||||||
|
resource={resource}
|
||||||
|
permissions={permissions}
|
||||||
|
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SheetFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={createMutation.isPending || updateMutation.isPending}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" loading={createMutation.isPending || updateMutation.isPending}>
|
||||||
|
{isEditing ? t("save") : t("create")}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</Form>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Badge } from "@calcom/ui/badge";
|
||||||
|
import { Button } from "@calcom/ui/button";
|
||||||
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@calcom/ui/components/dropdown";
|
||||||
|
|
||||||
|
import { useRoleStates } from "../hooks/useRoleQueryStates";
|
||||||
|
import { DeleteRoleModal } from "./DeleteRoleModal";
|
||||||
|
import { RoleSheet } from "./RoleSheet";
|
||||||
|
|
||||||
|
type Role = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
teamId?: number;
|
||||||
|
color?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
type: "SYSTEM" | "CUSTOM";
|
||||||
|
permissions: {
|
||||||
|
id: string;
|
||||||
|
resource: string;
|
||||||
|
action: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Roles = Role[];
|
||||||
|
|
||||||
|
type Permissions = {
|
||||||
|
canCreate: boolean;
|
||||||
|
canUpdate: boolean;
|
||||||
|
canDelete: boolean;
|
||||||
|
canRead: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RolesListProps {
|
||||||
|
roles: Roles;
|
||||||
|
permissions: Permissions;
|
||||||
|
initialSelectedRole?: Role;
|
||||||
|
initialSheetOpen?: boolean;
|
||||||
|
teamId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RolesList({
|
||||||
|
roles,
|
||||||
|
permissions,
|
||||||
|
initialSelectedRole,
|
||||||
|
initialSheetOpen,
|
||||||
|
teamId,
|
||||||
|
}: RolesListProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { isOpen, setIsOpen, selectedRoleId, setSelectedRoleId, handleSheetOpenChange } = useRoleStates(
|
||||||
|
initialSheetOpen,
|
||||||
|
initialSelectedRole?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedRole = roles.find((role) => role.id === selectedRoleId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="bg-muted border-muted flex flex-col rounded-xl border p-[1px]">
|
||||||
|
{/* Roles list header */}
|
||||||
|
<div className="px-5 py-4">
|
||||||
|
<h2 className="text-default text-sm font-semibold leading-none">{t("role")}</h2>
|
||||||
|
</div>
|
||||||
|
{/* Role List Items */}
|
||||||
|
<div className="bg-default border-subtle divide-subtle flex flex-col divide-y rounded-[10px] border">
|
||||||
|
{roles.map((role) => (
|
||||||
|
<RoleItem
|
||||||
|
role={role}
|
||||||
|
key={role.id}
|
||||||
|
onClick={() => {
|
||||||
|
// Cant edit system roles
|
||||||
|
if (role.type === "SYSTEM") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.canUpdate) {
|
||||||
|
setSelectedRoleId(role.id);
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
canUpdate={permissions.canUpdate}
|
||||||
|
permissions={permissions}
|
||||||
|
teamId={teamId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-5 py-4 ">
|
||||||
|
{/* Commenting out for now as we don't have a learn more permissions page in docs */}
|
||||||
|
{/* <p className="text-subtle text-sm font-medium leading-tight">{t("learn_more_permissions")}</p> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RoleSheet
|
||||||
|
role={selectedRole}
|
||||||
|
open={isOpen ?? false}
|
||||||
|
onOpenChange={handleSheetOpenChange}
|
||||||
|
teamId={teamId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RoleItem({
|
||||||
|
role,
|
||||||
|
onClick,
|
||||||
|
canUpdate,
|
||||||
|
permissions,
|
||||||
|
teamId,
|
||||||
|
}: {
|
||||||
|
role: Role;
|
||||||
|
onClick: () => void;
|
||||||
|
canUpdate: boolean;
|
||||||
|
permissions: Permissions;
|
||||||
|
teamId: number;
|
||||||
|
}) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const showDropdown = role.type !== "SYSTEM" && (permissions.canUpdate || permissions.canDelete);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"border-subtle flex p-3",
|
||||||
|
canUpdate && role.type !== "SYSTEM" && "hover:bg-subtle cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={onClick}>
|
||||||
|
<div className="flex w-full items-center gap-3 truncate">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="h-3 w-3 rounded-full" style={{ backgroundColor: role.color ?? "gray" }} />
|
||||||
|
</div>
|
||||||
|
{/* Role Name */}
|
||||||
|
<div className="text-default w-24 truncate text-sm font-semibold leading-none">
|
||||||
|
<span>{role.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Badge variant={role.type === "SYSTEM" ? "grayWithoutHover" : "green"}>
|
||||||
|
{t(role.type === "SYSTEM" ? "default_role" : "custom_role")}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{showDropdown && (
|
||||||
|
<div className="ml-auto" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="icon" color="secondary" StartIcon="ellipsis" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="flex flex-col">
|
||||||
|
{permissions.canUpdate && (
|
||||||
|
<DropdownItem StartIcon="pencil" onClick={onClick}>
|
||||||
|
{t("edit")}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
{permissions.canDelete && role.type !== "SYSTEM" && (
|
||||||
|
<DeleteRoleModal roleId={role.id} roleName={role.name} teamId={teamId} />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { SkeletonText } from "@calcom/ui/components/skeleton";
|
||||||
|
|
||||||
|
export const SkeletonLoader = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="bg-muted border-muted flex flex-col rounded-xl border p-[1px]">
|
||||||
|
{/* Roles list header */}
|
||||||
|
<div className="px-5 py-4">
|
||||||
|
<h2 className="text-default text-sm font-semibold leading-none">{t("role")}</h2>
|
||||||
|
</div>
|
||||||
|
{/* Role List Items */}
|
||||||
|
<div className="bg-default border-subtle divide divide-subtle flex flex-col rounded-[10px] border">
|
||||||
|
{/* Generate 3 skeleton items */}
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<div key={index} className="border-subtle flex border-b p-3">
|
||||||
|
<div className="flex items-center gap-3 truncate">
|
||||||
|
{/* Icon skeleton */}
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="h-3 w-3 animate-pulse rounded-full bg-gray-200" />
|
||||||
|
</div>
|
||||||
|
{/* Role name skeleton */}
|
||||||
|
<SkeletonText className="h-4 w-20" />
|
||||||
|
{/* Badge skeleton */}
|
||||||
|
<div className="h-5 w-24 animate-pulse rounded-full bg-gray-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="px-5 py-4 ">
|
||||||
|
<p className="text-subtle text-sm font-medium leading-tight">{t("learn_more_permissions")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { ToggleGroup } from "@calcom/ui/components/form";
|
||||||
|
|
||||||
|
import type { PermissionLevel } from "./usePermissions";
|
||||||
|
import { usePermissions } from "./usePermissions";
|
||||||
|
|
||||||
|
interface SimplePermissionItemProps {
|
||||||
|
resource: string;
|
||||||
|
permissions: string[];
|
||||||
|
onChange: (permissions: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SimplePermissionItem({ resource, permissions, onChange }: SimplePermissionItemProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions();
|
||||||
|
|
||||||
|
const isAllResources = resource === "*";
|
||||||
|
const options = isAllResources
|
||||||
|
? [
|
||||||
|
{ value: "none", label: t("none") },
|
||||||
|
{ value: "all", label: t("all") },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ value: "none", label: t("none") },
|
||||||
|
{ value: "read", label: t("read_only") },
|
||||||
|
{ value: "all", label: t("all") },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
|
<span className="text-default text-sm font-medium leading-none">
|
||||||
|
{t(PERMISSION_REGISTRY[resource as Resource]._resource?.i18nKey || resource)}
|
||||||
|
</span>
|
||||||
|
<ToggleGroup
|
||||||
|
onValueChange={(val) => {
|
||||||
|
if (val) onChange(toggleResourcePermissionLevel(resource, val as PermissionLevel, permissions));
|
||||||
|
}}
|
||||||
|
value={getResourcePermissionLevel(resource, permissions)}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { createSearchParamsCache, parseAsBoolean, parseAsString } from "nuqs/server";
|
||||||
|
|
||||||
|
export const roleParsers = {
|
||||||
|
// Sheet visibility state
|
||||||
|
"role-sheet": parseAsBoolean.withDefault(false),
|
||||||
|
// Selected role ID for editing
|
||||||
|
role: parseAsString.withDefault(""),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const roleSearchParamsCache = createSearchParamsCache(roleParsers);
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
import { CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
|
||||||
|
export type PermissionLevel = "none" | "read" | "all";
|
||||||
|
|
||||||
|
interface UsePermissionsReturn {
|
||||||
|
hasAllPermissions: (permissions: string[]) => boolean;
|
||||||
|
getResourcePermissionLevel: (resource: string, permissions: string[]) => PermissionLevel;
|
||||||
|
toggleResourcePermissionLevel: (
|
||||||
|
resource: string,
|
||||||
|
level: PermissionLevel,
|
||||||
|
currentPermissions: string[]
|
||||||
|
) => string[];
|
||||||
|
toggleSinglePermission: (permission: string, enabled: boolean, currentPermissions: string[]) => string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePermissions(): UsePermissionsReturn {
|
||||||
|
const getAllPossiblePermissions = () => {
|
||||||
|
const permissions: string[] = [];
|
||||||
|
Object.entries(PERMISSION_REGISTRY).forEach(([resource, config]) => {
|
||||||
|
if (resource !== "*") {
|
||||||
|
Object.keys(config).forEach((action) => {
|
||||||
|
permissions.push(`${resource}.${action}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return permissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAllPermissions = (permissions: string[]) => {
|
||||||
|
return Object.entries(PERMISSION_REGISTRY).every(([resource, config]) => {
|
||||||
|
if (resource === "*") return true;
|
||||||
|
return Object.keys(config).every((action) => permissions.includes(`${resource}.${action}`));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResourcePermissionLevel = (resource: string, permissions: string[]): PermissionLevel => {
|
||||||
|
if (resource === "*") {
|
||||||
|
return permissions.includes("*.*") ? "all" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY];
|
||||||
|
if (!resourceConfig) return "none";
|
||||||
|
|
||||||
|
const allResourcePerms = Object.keys(resourceConfig).map((action) => `${resource}.${action}`);
|
||||||
|
const hasAllPerms = allResourcePerms.every((p) => permissions.includes(p));
|
||||||
|
const hasReadPerm = permissions.includes(`${resource}.${CrudAction.Read}`);
|
||||||
|
|
||||||
|
if (hasAllPerms) return "all";
|
||||||
|
if (hasReadPerm) return "read";
|
||||||
|
return "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleResourcePermissionLevel = (
|
||||||
|
resource: string,
|
||||||
|
level: PermissionLevel,
|
||||||
|
currentPermissions: string[]
|
||||||
|
): string[] => {
|
||||||
|
// First, remove *.* since we're modifying permissions
|
||||||
|
let newPermissions = currentPermissions.filter((p) => p !== "*.*");
|
||||||
|
|
||||||
|
if (resource === "*") {
|
||||||
|
if (level === "all") {
|
||||||
|
// When selecting all resources, add *.* and all individual permissions
|
||||||
|
const allPossiblePerms = getAllPossiblePermissions();
|
||||||
|
newPermissions = ["*.*", ...allPossiblePerms];
|
||||||
|
}
|
||||||
|
// When deselecting all resources, we've already removed *.* above
|
||||||
|
} else {
|
||||||
|
// Filter out current resource permissions
|
||||||
|
newPermissions = newPermissions.filter((p) => !p.startsWith(`${resource}.`));
|
||||||
|
const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY];
|
||||||
|
|
||||||
|
if (!resourceConfig) return currentPermissions;
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case "none":
|
||||||
|
// No permissions to add, just keep other permissions
|
||||||
|
break;
|
||||||
|
case "read":
|
||||||
|
// Only add read permission
|
||||||
|
newPermissions.push(`${resource}.${CrudAction.Read}`);
|
||||||
|
break;
|
||||||
|
case "all":
|
||||||
|
// Add all permissions for this resource
|
||||||
|
const allResourcePerms = Object.keys(resourceConfig).map((action) => `${resource}.${action}`);
|
||||||
|
newPermissions.push(...allResourcePerms);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add *.* back if all permissions are now selected
|
||||||
|
if (hasAllPermissions(newPermissions)) {
|
||||||
|
newPermissions.push("*.*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPermissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSinglePermission = (
|
||||||
|
permission: string,
|
||||||
|
enabled: boolean,
|
||||||
|
currentPermissions: string[]
|
||||||
|
): string[] => {
|
||||||
|
// First, remove *.* since we're modifying individual permissions
|
||||||
|
let newPermissions = currentPermissions.filter((p) => p !== "*.*");
|
||||||
|
|
||||||
|
// Parse the permission to get resource and action
|
||||||
|
const [resource, action] = permission.split(".");
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
// Add the requested permission
|
||||||
|
newPermissions.push(permission);
|
||||||
|
|
||||||
|
// If enabling create, update, or delete, automatically enable read permission
|
||||||
|
if (action === CrudAction.Create || action === CrudAction.Update || action === CrudAction.Delete) {
|
||||||
|
const readPermission = `${resource}.${CrudAction.Read}`;
|
||||||
|
if (!newPermissions.includes(readPermission)) {
|
||||||
|
newPermissions.push(readPermission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// When disabling a permission, check if we need to disable related permissions
|
||||||
|
if (action === CrudAction.Read) {
|
||||||
|
// If disabling read, also disable create, update, and delete since they depend on read
|
||||||
|
const dependentActions = [CrudAction.Create, CrudAction.Update, CrudAction.Delete];
|
||||||
|
dependentActions.forEach((dependentAction) => {
|
||||||
|
const dependentPermission = `${resource}.${dependentAction}`;
|
||||||
|
newPermissions = newPermissions.filter((p) => p !== dependentPermission);
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
action === CrudAction.Create ||
|
||||||
|
action === CrudAction.Update ||
|
||||||
|
action === CrudAction.Delete
|
||||||
|
) {
|
||||||
|
// If disabling create, update, or delete, just remove that specific permission
|
||||||
|
// Read permission remains enabled
|
||||||
|
newPermissions = newPermissions.filter((p) => p !== permission);
|
||||||
|
} else {
|
||||||
|
// For other actions (custom actions), just remove the specific permission
|
||||||
|
newPermissions = newPermissions.filter((p) => p !== permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add *.* back if all permissions are now selected
|
||||||
|
if (hasAllPermissions(newPermissions)) {
|
||||||
|
newPermissions.push("*.*");
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPermissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasAllPermissions,
|
||||||
|
getResourcePermissionLevel,
|
||||||
|
toggleResourcePermissionLevel,
|
||||||
|
toggleSinglePermission,
|
||||||
|
};
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
export async function revalidateTeamRoles() {
|
||||||
|
revalidatePath("/settings/organizations/roles");
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQueryState } from "nuqs";
|
||||||
|
|
||||||
|
import { roleParsers } from "../_components/searchParams";
|
||||||
|
|
||||||
|
export function useRoleSheetState(initialOpen?: boolean) {
|
||||||
|
const [isOpen, setIsOpen] = useQueryState("role-sheet", roleParsers["role-sheet"]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSelectedRoleState(initialRoleId?: string) {
|
||||||
|
const [selectedRoleId, setSelectedRoleId] = useQueryState("role", roleParsers.role);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedRoleId,
|
||||||
|
setSelectedRoleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRoleStates(initialOpen?: boolean, initialRoleId?: string) {
|
||||||
|
const { isOpen, setIsOpen } = useRoleSheetState(initialOpen);
|
||||||
|
const { selectedRoleId, setSelectedRoleId } = useSelectedRoleState(initialRoleId);
|
||||||
|
|
||||||
|
const handleSheetOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setSelectedRoleId("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
selectedRoleId,
|
||||||
|
setSelectedRoleId,
|
||||||
|
handleSheetOpenChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { SkeletonLoader } from "./_components/RolesSkeletonLoader";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <SkeletonLoader />;
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||||
|
import { unstable_cache } from "next/cache";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import type { AppFlags } from "@calcom/features/flags/config";
|
||||||
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
|
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
|
||||||
|
import { Resource, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||||
|
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||||
|
import { RoleService } from "@calcom/features/pbac/services/role.service";
|
||||||
|
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||||
|
|
||||||
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
|
import { CreateRoleCTA } from "./_components/CreateRoleCta";
|
||||||
|
import { RolesList } from "./_components/RolesList";
|
||||||
|
import { roleSearchParamsCache } from "./_components/searchParams";
|
||||||
|
|
||||||
|
const getCachedTeamRoles = unstable_cache(
|
||||||
|
async (teamId: number) => {
|
||||||
|
const roleService = new RoleService();
|
||||||
|
return roleService.getTeamRoles(teamId);
|
||||||
|
},
|
||||||
|
["team-roles"],
|
||||||
|
{ revalidate: 3600 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCachedTeamFeature = unstable_cache(
|
||||||
|
async (teamId: number, feature: keyof AppFlags) => {
|
||||||
|
const featureRepo = new FeaturesRepository();
|
||||||
|
const res = await featureRepo.checkIfTeamHasFeature(teamId, feature);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
["team-feature"],
|
||||||
|
{ revalidate: 3600 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCachedResourcePermissions = unstable_cache(
|
||||||
|
async (userId: number, teamId: number, resource: Resource) => {
|
||||||
|
const permissionService = new PermissionCheckService();
|
||||||
|
return permissionService.getResourcePermissions({ userId, teamId, resource });
|
||||||
|
},
|
||||||
|
["resource-permissions"],
|
||||||
|
{ revalidate: 3600 }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("roles_and_permissions"),
|
||||||
|
(t) => t("roles_and_permissions_description"),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"/settings/organizations/roles"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Page = async ({ searchParams }: { searchParams: Record<string, string | string[] | undefined> }) => {
|
||||||
|
const t = await getTranslate();
|
||||||
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
|
|
||||||
|
if (!session?.user?.org?.id || !session.user.id) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamHasPBACFeature = await getCachedTeamFeature(session.user.org.id, "pbac");
|
||||||
|
|
||||||
|
if (!teamHasPBACFeature) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
roleSearchParamsCache.parse(searchParams);
|
||||||
|
|
||||||
|
const [roles, rolePermissions] = await Promise.all([
|
||||||
|
getCachedTeamRoles(session.user.org.id),
|
||||||
|
getCachedResourcePermissions(session.user.id, session.user.org.id, Resource.Role),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// NOTE: this approach of fetching permssions per resource does not account for fall back roles.
|
||||||
|
const roleActions = PermissionMapper.toActionMap(rolePermissions, Resource.Role);
|
||||||
|
|
||||||
|
const canCreate = roleActions[CrudAction.Create] ?? false;
|
||||||
|
const canRead = roleActions[CrudAction.Read] ?? false;
|
||||||
|
const canUpdate = roleActions[CrudAction.Update] ?? false;
|
||||||
|
const canDelete = roleActions[CrudAction.Delete] ?? false;
|
||||||
|
|
||||||
|
if (!canRead) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sheet state from server cache
|
||||||
|
const isSheetOpen = roleSearchParamsCache.get("role-sheet");
|
||||||
|
const selectedRoleId = roleSearchParamsCache.get("role");
|
||||||
|
const selectedRole = roles.find((role) => role.id === selectedRoleId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsHeader
|
||||||
|
title={t("roles_and_permissions")}
|
||||||
|
description={t("roles_and_permissions_description")}
|
||||||
|
borderInShellHeader={false}
|
||||||
|
CTA={canCreate ? <CreateRoleCTA /> : null}>
|
||||||
|
<RolesList
|
||||||
|
teamId={session.user.org.id}
|
||||||
|
roles={roles}
|
||||||
|
permissions={{
|
||||||
|
canCreate: canCreate,
|
||||||
|
canRead: canRead,
|
||||||
|
canUpdate: canUpdate,
|
||||||
|
canDelete: canDelete,
|
||||||
|
}}
|
||||||
|
initialSelectedRole={selectedRole}
|
||||||
|
initialSheetOpen={isSheetOpen}
|
||||||
|
/>
|
||||||
|
</SettingsHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+14
-2
@@ -2,8 +2,8 @@ import { createRouterCaller } from "app/_trpc/context";
|
|||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { unstable_cache } from "next/cache";
|
import { unstable_cache } from "next/cache";
|
||||||
|
|
||||||
|
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||||
import { AttributeRepository } from "@calcom/lib/server/repository/attribute";
|
import { AttributeRepository } from "@calcom/lib/server/repository/attribute";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
|
||||||
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
||||||
|
|
||||||
import { MembersView } from "~/members/members-view";
|
import { MembersView } from "~/members/members-view";
|
||||||
@@ -25,12 +25,24 @@ const getCachedAttributes = unstable_cache(
|
|||||||
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getCachedRoles = unstable_cache(
|
||||||
|
async (orgId: number) => {
|
||||||
|
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(orgId);
|
||||||
|
return await roleManager.getAllRoles(orgId);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ revalidate: 3600, tags: ["pbac.roles.list"] } // Cache for 1 hour
|
||||||
|
);
|
||||||
|
|
||||||
const Page = async () => {
|
const Page = async () => {
|
||||||
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
|
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
|
||||||
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
|
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
|
||||||
const attributes = await getCachedAttributes(org.id);
|
const attributes = await getCachedAttributes(org.id);
|
||||||
|
|
||||||
|
const roles = await getCachedRoles(org.id);
|
||||||
|
|
||||||
const facetedTeamValues = {
|
const facetedTeamValues = {
|
||||||
roles: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
roles,
|
||||||
teams,
|
teams,
|
||||||
attributes: attributes.map((attribute) => ({
|
attributes: attributes.map((attribute) => ({
|
||||||
id: attribute.id,
|
id: attribute.id,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { notFound } from "next/navigation";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||||
import { getServerSideProps } from "@lib/video/meeting-not-started/[uid]/getServerSideProps";
|
import { getServerSideProps } from "@lib/video/meeting-not-started/[uid]/getServerSideProps";
|
||||||
@@ -22,7 +23,8 @@ export const generateMetadata = async ({ params }: ServerPageProps) => {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
const booking = await BookingRepository.findBookingByUid({
|
const bookingRepo = new BookingRepository(prisma);
|
||||||
|
const booking = await bookingRepo.findBookingByUid({
|
||||||
bookingUid: parsed.data.uid,
|
bookingUid: parsed.data.uid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
|||||||
import notEmpty from "@calcom/lib/notEmpty";
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
@@ -29,7 +30,10 @@ async function authMiddleware() {
|
|||||||
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userWithCredentials = await UserRepository.findUserWithCredentials({ id: session.user.id });
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const userWithCredentials = await userRepo.findUserWithCredentials({
|
||||||
|
id: session.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (!userWithCredentials) {
|
if (!userWithCredentials) {
|
||||||
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
|
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { CardComponent } from "@lib/plain/card-components";
|
import { CardComponent } from "@lib/plain/card-components";
|
||||||
@@ -474,7 +475,8 @@ async function handler(request: NextRequest) {
|
|||||||
// Validate request body
|
// Validate request body
|
||||||
const { cardKeys, customer } = inputSchema.parse(requestBody);
|
const { cardKeys, customer } = inputSchema.parse(requestBody);
|
||||||
|
|
||||||
const user = await UserRepository.findByEmail({ email: customer.email });
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const user = await userRepo.findByEmail({ email: customer.email });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -502,7 +504,7 @@ async function handler(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch team details including userId and team name
|
// Fetch team details including userId and team name
|
||||||
const teamMemberships = await UserRepository.findTeamsByUserId({ userId: user.id });
|
const teamMemberships = await userRepo.findTeamsByUserId({ userId: user.id });
|
||||||
const firstTeam = teamMemberships.teams[0] ?? null;
|
const firstTeam = teamMemberships.teams[0] ?? null;
|
||||||
|
|
||||||
// Parse user metadata
|
// Parse user metadata
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
import { beforeEach, describe, it, expect, vi } from "vitest";
|
||||||
|
|
||||||
import { onSubmissionOfFormResponse } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils";
|
import { onSubmissionOfFormResponse } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils";
|
||||||
import { getResponseToStore } from "@calcom/app-store/routing-forms/lib/getResponseToStore";
|
import { getResponseToStore } from "@calcom/app-store/routing-forms/lib/getResponseToStore";
|
||||||
@@ -8,6 +8,11 @@ import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/for
|
|||||||
import { queuedResponseHandler } from "../route";
|
import { queuedResponseHandler } from "../route";
|
||||||
|
|
||||||
vi.mock("@calcom/lib/server/repository/formResponse");
|
vi.mock("@calcom/lib/server/repository/formResponse");
|
||||||
|
|
||||||
|
const mockRoutingFormResponseRepository = {
|
||||||
|
getQueuedFormResponseFromId: vi.fn(),
|
||||||
|
recordFormResponse: vi.fn(),
|
||||||
|
};
|
||||||
vi.mock("@calcom/app-store/routing-forms/lib/getSerializableForm");
|
vi.mock("@calcom/app-store/routing-forms/lib/getSerializableForm");
|
||||||
vi.mock("@calcom/app-store/routing-forms/lib/getResponseToStore");
|
vi.mock("@calcom/app-store/routing-forms/lib/getResponseToStore");
|
||||||
vi.mock("@calcom/app-store/routing-forms/lib/formSubmissionUtils");
|
vi.mock("@calcom/app-store/routing-forms/lib/formSubmissionUtils");
|
||||||
@@ -43,8 +48,14 @@ const mockQueuedFormResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("queuedResponseHandler", () => {
|
describe("queuedResponseHandler", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(RoutingFormResponseRepository).mockImplementation(
|
||||||
|
() => mockRoutingFormResponseRepository as any
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should process a queued form response", async () => {
|
it("should process a queued form response", async () => {
|
||||||
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
|
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
|
||||||
mockQueuedFormResponse
|
mockQueuedFormResponse
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -90,7 +101,7 @@ describe("queuedResponseHandler", () => {
|
|||||||
team: null,
|
team: null,
|
||||||
} as unknown as Awaited<ReturnType<typeof onSubmissionOfFormResponse>>);
|
} as unknown as Awaited<ReturnType<typeof onSubmissionOfFormResponse>>);
|
||||||
|
|
||||||
vi.mocked(RoutingFormResponseRepository.recordFormResponse).mockResolvedValue({
|
vi.mocked(mockRoutingFormResponseRepository.recordFormResponse).mockResolvedValue({
|
||||||
id: "mock-form-id",
|
id: "mock-form-id",
|
||||||
name: "Test Form",
|
name: "Test Form",
|
||||||
description: "Test Form Description",
|
description: "Test Form Description",
|
||||||
@@ -102,7 +113,7 @@ describe("queuedResponseHandler", () => {
|
|||||||
},
|
},
|
||||||
team: null,
|
team: null,
|
||||||
chosenRouteId: "mock-chosen-route-id",
|
chosenRouteId: "mock-chosen-route-id",
|
||||||
} as unknown as Awaited<ReturnType<typeof RoutingFormResponseRepository.recordFormResponse>>);
|
} as unknown as Awaited<ReturnType<typeof mockRoutingFormResponseRepository.recordFormResponse>>);
|
||||||
|
|
||||||
const response = await queuedResponseHandler({
|
const response = await queuedResponseHandler({
|
||||||
queuedFormResponseId: "1",
|
queuedFormResponseId: "1",
|
||||||
@@ -115,7 +126,7 @@ describe("queuedResponseHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("if no queued form response is found, should return early", async () => {
|
it("if no queued form response is found, should return early", async () => {
|
||||||
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(null);
|
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(null);
|
||||||
const response = await queuedResponseHandler({
|
const response = await queuedResponseHandler({
|
||||||
queuedFormResponseId: "1",
|
queuedFormResponseId: "1",
|
||||||
params: {},
|
params: {},
|
||||||
@@ -127,7 +138,7 @@ describe("queuedResponseHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw if form has no fields", async () => {
|
it("should throw if form has no fields", async () => {
|
||||||
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
|
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
|
||||||
mockQueuedFormResponse
|
mockQueuedFormResponse
|
||||||
);
|
);
|
||||||
vi.mocked(getSerializableForm).mockResolvedValue({
|
vi.mocked(getSerializableForm).mockResolvedValue({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSeri
|
|||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
|
import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { defaultResponderForAppDir } from "../../defaultResponderForAppDir";
|
import { defaultResponderForAppDir } from "../../defaultResponderForAppDir";
|
||||||
|
|
||||||
@@ -23,10 +24,10 @@ export const queuedResponseHandler = async ({
|
|||||||
queuedFormResponseId: string;
|
queuedFormResponseId: string;
|
||||||
params: Record<string, string | string[]>;
|
params: Record<string, string | string[]>;
|
||||||
}) => {
|
}) => {
|
||||||
|
const formResponseRepo = new RoutingFormResponseRepository(prisma);
|
||||||
|
|
||||||
// Get the queued response
|
// Get the queued response
|
||||||
const queuedFormResponse = await RoutingFormResponseRepository.getQueuedFormResponseFromId(
|
const queuedFormResponse = await formResponseRepo.getQueuedFormResponseFromId(queuedFormResponseId);
|
||||||
queuedFormResponseId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!queuedFormResponse) {
|
if (!queuedFormResponse) {
|
||||||
return {
|
return {
|
||||||
@@ -48,7 +49,7 @@ export const queuedResponseHandler = async ({
|
|||||||
fieldsResponses: params,
|
fieldsResponses: params,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formResponse = await RoutingFormResponseRepository.recordFormResponse({
|
const formResponse = await formResponseRepo.recordFormResponse({
|
||||||
formId: queuedFormResponse.formId,
|
formId: queuedFormResponse.formId,
|
||||||
queuedFormResponseId: queuedFormResponse.id,
|
queuedFormResponseId: queuedFormResponse.id,
|
||||||
// We record new response here as that might be different from the queued response depending on if the user changed something in b/w before clicking CTA and that something wasn't prerendered
|
// We record new response here as that might be different from the queued response depending on if the user changed something in b/w before clicking CTA and that something wasn't prerendered
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
data-testid="confirm_cancel"
|
data-testid="confirm_cancel"
|
||||||
disabled={
|
disabled={
|
||||||
props.isHost &&
|
props.isHost &&
|
||||||
(!cancellationReason || (props.internalNotePresets.length > 0 && !internalNote?.id))
|
(!cancellationReason?.trim() || (props.internalNotePresets.length > 0 && !internalNote?.id))
|
||||||
}
|
}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -440,7 +440,11 @@ describe("RerouteDialog", () => {
|
|||||||
render(
|
render(
|
||||||
<SessionProvider session={mockSession}>
|
<SessionProvider session={mockSession}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
<RerouteDialog
|
||||||
|
isOpenDialog={true}
|
||||||
|
setIsOpenDialog={mockSetIsOpenDialog}
|
||||||
|
booking={mockBooking}
|
||||||
|
/>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
@@ -483,7 +487,11 @@ describe("RerouteDialog", () => {
|
|||||||
render(
|
render(
|
||||||
<SessionProvider session={mockSession}>
|
<SessionProvider session={mockSession}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
<RerouteDialog
|
||||||
|
isOpenDialog={true}
|
||||||
|
setIsOpenDialog={mockSetIsOpenDialog}
|
||||||
|
booking={mockBooking}
|
||||||
|
/>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
|||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button } from "@calcom/ui/components/button";
|
||||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||||
import { EmailField, Label, TextField, PasswordField } from "@calcom/ui/components/form";
|
import { EmailField, Label, TextField, PasswordField } from "@calcom/ui/components/form";
|
||||||
|
|
||||||
@@ -34,7 +35,12 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
|
|||||||
return <AdminUser {...props} />;
|
return <AdminUser {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
|
export const AdminUser = (props: {
|
||||||
|
onSubmit: () => void;
|
||||||
|
onError: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
nav: { onNext: () => void; onPrev: () => void };
|
||||||
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -118,7 +124,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
|
|||||||
<TextField
|
<TextField
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
!longWebsiteUrl && (
|
!longWebsiteUrl && (
|
||||||
<span className="text-subtle inline-flex items-center rounded-none px-3 text-sm">
|
<span className="text-subtle inline-flex items-center rounded-none text-sm">
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -187,6 +193,15 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
loading={formMethods.formState.isSubmitting}
|
||||||
|
disabled={!formMethods.formState.isValid || formMethods.formState.isSubmitting}>
|
||||||
|
{t("next")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
import { TextField } from "@calcom/ui/components/form";
|
||||||
|
import { Icon } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
type LicenseSelectionFormValues = {
|
||||||
|
licenseKey: string;
|
||||||
|
signatureToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LicenseOption = "FREE" | "EXISTING";
|
||||||
|
|
||||||
|
const LicenseSelection = (
|
||||||
|
props: {
|
||||||
|
value: LicenseOption;
|
||||||
|
onChange: (value: LicenseOption) => void;
|
||||||
|
onSubmit: (value: LicenseSelectionFormValues) => void;
|
||||||
|
onSuccess?: (
|
||||||
|
data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
|
||||||
|
variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
|
||||||
|
) => void;
|
||||||
|
onPrevStep: () => void;
|
||||||
|
onNextStep: () => void;
|
||||||
|
} & Omit<JSX.IntrinsicElements["form"], "onSubmit" | "onChange">
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
value: initialValue = "FREE",
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
onSuccess,
|
||||||
|
onPrevStep,
|
||||||
|
onNextStep,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
const [value, setValue] = useState<LicenseOption>(initialValue);
|
||||||
|
const { t } = useLocale();
|
||||||
|
const mutation = trpc.viewer.deploymentSetup.update.useMutation({
|
||||||
|
onSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [licenseKeyInput, setLicenseKeyInput] = useState("");
|
||||||
|
const [licenseTouched, setLicenseTouched] = useState(false);
|
||||||
|
|
||||||
|
// Use TRPC query for validation
|
||||||
|
const { data: licenseValidation, isLoading: checkLicenseLoading } =
|
||||||
|
trpc.viewer.deploymentSetup.validateLicense.useQuery(
|
||||||
|
{ licenseKey: licenseKeyInput },
|
||||||
|
{
|
||||||
|
enabled: licenseTouched && licenseKeyInput.trim().length > 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const schemaLicenseKey = useCallback(
|
||||||
|
() =>
|
||||||
|
z.object({
|
||||||
|
licenseKey: z
|
||||||
|
.string()
|
||||||
|
.min(1, t("license_key_required"))
|
||||||
|
.refine(() => !licenseTouched || (licenseValidation ? licenseValidation.valid : true), {
|
||||||
|
message: licenseValidation?.message || t("invalid_license_key"),
|
||||||
|
}),
|
||||||
|
signatureToken: z.string().optional(),
|
||||||
|
}),
|
||||||
|
[licenseValidation, licenseTouched, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formMethods = useForm<LicenseSelectionFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
licenseKey: "",
|
||||||
|
signatureToken: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schemaLicenseKey()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = formMethods.handleSubmit((values) => {
|
||||||
|
onSubmit(values);
|
||||||
|
if (value === "EXISTING" && values.licenseKey) {
|
||||||
|
mutation.mutate(values);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isDirty, errors } = useFormState(formMethods);
|
||||||
|
|
||||||
|
const handleRadioChange = (newValue: LicenseOption) => {
|
||||||
|
setValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
{...rest}
|
||||||
|
className="space-y-6"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (value === "FREE") {
|
||||||
|
onSubmit({ licenseKey: "", signatureToken: "" });
|
||||||
|
} else {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Root
|
||||||
|
defaultValue={initialValue}
|
||||||
|
value={value}
|
||||||
|
aria-label={t("choose_a_license")}
|
||||||
|
className="grid grid-rows-2 gap-4 md:grid-cols-2 md:grid-rows-1"
|
||||||
|
onValueChange={(value) => handleRadioChange(value as LicenseOption)}>
|
||||||
|
<RadioGroup.Item value="FREE" className="h-full">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"bg-default h-full cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
|
||||||
|
value === "FREE" && "ring-2 ring-black"
|
||||||
|
)}>
|
||||||
|
<h2 className="font-cal text-emphasis text-xl">{t("agplv3_license")}</h2>
|
||||||
|
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
|
||||||
|
<p className="text-subtle">{t("forever_open_and_free")}</p>
|
||||||
|
<ul className="text-subtle ml-4 list-disc text-left text-xs">
|
||||||
|
<li>{t("required_to_keep_your_code_open_source")}</li>
|
||||||
|
<li>{t("cannot_repackage_and_resell")}</li>
|
||||||
|
<li>{t("no_enterprise_features")}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
|
||||||
|
<RadioGroup.Item value="EXISTING" className="h-full">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"bg-default h-full cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
|
||||||
|
value === "EXISTING" && "ring-2 ring-black"
|
||||||
|
)}>
|
||||||
|
<h2 className="font-cal text-emphasis text-xl">{t("enter_license_key")}</h2>
|
||||||
|
<p className="font-medium text-green-800">{t("enter_existing_license")}</p>
|
||||||
|
<p className="text-subtle">{t("enter_your_license_key")}</p>
|
||||||
|
<p className="text-subtle text-xs">
|
||||||
|
{t("need_a_license")}{" "}
|
||||||
|
<Link
|
||||||
|
href="https://go.cal.com/self-hosted"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
className="text-blue-600 hover:underline">
|
||||||
|
{t("purchase_license")}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
|
||||||
|
{value === "EXISTING" && (
|
||||||
|
<FormProvider {...formMethods}>
|
||||||
|
<div className="bg-muted space-y-4 rounded-md px-4 py-3">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
name="licenseKey"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { onBlur, onChange, value } }) => (
|
||||||
|
<TextField
|
||||||
|
name="licenseKey"
|
||||||
|
label={t("license_key")}
|
||||||
|
className={classNames(
|
||||||
|
"group-hover:border-emphasis mb-0",
|
||||||
|
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
|
||||||
|
)}
|
||||||
|
placeholder="cal_live_XXXXXXXXXXX"
|
||||||
|
value={licenseKeyInput}
|
||||||
|
addOnClassname={classNames(
|
||||||
|
"hover:border-default",
|
||||||
|
errors.licenseKey === undefined && isDirty && "group-hover:border-emphasis"
|
||||||
|
)}
|
||||||
|
addOnSuffix={
|
||||||
|
checkLicenseLoading ? (
|
||||||
|
<Icon name="loader" className="h-5 w-5 animate-spin" />
|
||||||
|
) : licenseValidation?.valid && licenseTouched ? (
|
||||||
|
<Icon name="check" className="h-5 w-5 text-green-700" />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
color={errors.licenseKey ? "warn" : ""}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setLicenseTouched(true);
|
||||||
|
onBlur();
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLicenseKeyInput(e.target.value);
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.licenseKey && (
|
||||||
|
<p className="mt-1 text-sm text-red-600">{errors.licenseKey.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
name="signatureToken"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { onBlur, onChange, value } }) => (
|
||||||
|
<TextField
|
||||||
|
name="signatureToken"
|
||||||
|
label={t("signature_token_optional")}
|
||||||
|
placeholder="cal_sk_XXXXXXXXXXX"
|
||||||
|
value={value || ""}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
formMethods.setValue("signatureToken", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className="text-subtle mt-1 text-sm">{t("signature_token_description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button type="button" color="secondary" onClick={onPrevStep}>
|
||||||
|
{t("prev_step")}
|
||||||
|
</Button>
|
||||||
|
{value === "EXISTING" ? (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
loading={checkLicenseLoading || mutation.isPending}
|
||||||
|
disabled={
|
||||||
|
value === "EXISTING" &&
|
||||||
|
(!formMethods.formState.isValid || checkLicenseLoading || mutation.isPending)
|
||||||
|
}>
|
||||||
|
{t("save_license_key")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button color="primary" onClick={onNextStep} type="button">
|
||||||
|
{t("next_step")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LicenseSelection;
|
||||||
@@ -18,7 +18,8 @@ import { STEPS } from "~/apps/installation/[[...step]]/constants";
|
|||||||
import type { OnboardingPageProps, TEventTypeGroup } from "~/apps/installation/[[...step]]/step-view";
|
import type { OnboardingPageProps, TEventTypeGroup } from "~/apps/installation/[[...step]]/step-view";
|
||||||
|
|
||||||
const getUser = async (userId: number) => {
|
const getUser = async (userId: number) => {
|
||||||
const userAdminTeams = await UserRepository.getUserAdminTeams(userId);
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const userAdminTeams = await userRepo.getUserAdminTeams({ userId });
|
||||||
|
|
||||||
if (!userAdminTeams?.id) {
|
if (!userAdminTeams?.id) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
name = profileUsername || username;
|
name = profileUsername || username;
|
||||||
|
|
||||||
const [user] = await UserRepository.findUsersByUsername({
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const [user] = await userRepo.findUsersByUsername({
|
||||||
usernameList: [name],
|
usernameList: [name],
|
||||||
orgSlug: org,
|
orgSlug: org,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req } = context;
|
const { req } = context;
|
||||||
@@ -12,7 +13,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await UserRepository.findUserTeams({
|
const userRepo = new UserRepository(prisma);
|
||||||
|
const user = await userRepo.findUserTeams({
|
||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -101,8 +101,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||||
|
|
||||||
|
const userRepo = new UserRepository(prisma);
|
||||||
const enrichedBookingUser = booking.user
|
const enrichedBookingUser = booking.user
|
||||||
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
|
? await userRepo.enrichUserWithItsProfile({ user: booking.user })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const eventUrl = await buildEventUrlFromBooking({
|
const eventUrl = await buildEventUrlFromBooking({
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ const checkIfUserIsHost = async ({
|
|||||||
return booking.user?.id === sessionUserId;
|
return booking.user?.id === sessionUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventType = await EventTypeRepository.findByIdWithUserAccess({
|
const eventTypeRepo = new EventTypeRepository(prisma);
|
||||||
|
const eventType = await eventTypeRepo.findByIdWithUserAccess({
|
||||||
id: booking.eventTypeId,
|
id: booking.eventTypeId,
|
||||||
userId: sessionUserId,
|
userId: sessionUserId,
|
||||||
});
|
});
|
||||||
@@ -106,7 +107,8 @@ const checkIfUserIsHost = async ({
|
|||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const { req } = context;
|
const { req } = context;
|
||||||
|
|
||||||
const booking = await BookingRepository.findBookingForMeetingPage({
|
const bookingRepo = new BookingRepository(prisma);
|
||||||
|
const booking = await bookingRepo.findBookingForMeetingPage({
|
||||||
bookingUid: context.query.uid as string,
|
bookingUid: context.query.uid as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,9 +142,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
})
|
})
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
const userRepo = new UserRepository(prisma);
|
||||||
const profile = booking.user
|
const profile = booking.user
|
||||||
? (
|
? (
|
||||||
await UserRepository.enrichUserWithItsProfile({
|
await userRepo.enrichUserWithItsProfile({
|
||||||
user: booking.user,
|
user: booking.user,
|
||||||
})
|
})
|
||||||
).profile
|
).profile
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user