Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
260f4e1b51 | ||
|
|
e6c36af3b4 | ||
|
|
2fb2458eee | ||
|
|
b340b0edbe | ||
|
|
f7fd1c4402 | ||
|
|
c316721051 | ||
|
|
80ac118242 | ||
|
|
efd15c5a3e | ||
|
|
7d02d1bb23 | ||
|
|
0d877e7be3 | ||
|
|
e4c47640fc | ||
|
|
89e7fca183 | ||
|
|
33b5bf1db8 | ||
|
|
f69c4a900e | ||
|
|
1da25cc85f | ||
|
|
3a65b372c5 | ||
|
|
9892d521fb | ||
|
|
3285b252c2 |
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
@@ -105,7 +105,9 @@ async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||
const usersWithCalendars = await new UserRepository(
|
||||
prisma
|
||||
).findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||
ids: userIds,
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.249",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.256",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -109,8 +109,9 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
||||
cancellationReason:
|
||||
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
@@ -204,8 +205,9 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
||||
cancellationReason:
|
||||
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
@@ -266,7 +268,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
cancellationReason:
|
||||
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
@@ -375,7 +378,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
cancellationReason:
|
||||
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
|
||||
@@ -21402,7 +21402,8 @@
|
||||
"OOO_CREATED",
|
||||
"AFTER_HOSTS_CAL_VIDEO_NO_SHOW",
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW",
|
||||
"FORM_SUBMITTED_NO_EVENT"
|
||||
"FORM_SUBMITTED_NO_EVENT",
|
||||
"RESERVATION_EXPIRED"
|
||||
]
|
||||
},
|
||||
"secret": {
|
||||
@@ -21479,7 +21480,8 @@
|
||||
"OOO_CREATED",
|
||||
"AFTER_HOSTS_CAL_VIDEO_NO_SHOW",
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW",
|
||||
"FORM_SUBMITTED_NO_EVENT"
|
||||
"FORM_SUBMITTED_NO_EVENT",
|
||||
"RESERVATION_EXPIRED"
|
||||
]
|
||||
},
|
||||
"secret": {
|
||||
@@ -23975,7 +23977,7 @@
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"Attendee": {
|
||||
"CreateBookingAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -24112,7 +24114,7 @@
|
||||
"description": "The attendee's details.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -24239,7 +24241,7 @@
|
||||
"description": "The attendee's details.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -24372,7 +24374,7 @@
|
||||
"description": "The attendee's details.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -24541,6 +24543,85 @@
|
||||
"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"
|
||||
],
|
||||
"example": "en"
|
||||
},
|
||||
"absent": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"timeZone",
|
||||
"absent"
|
||||
]
|
||||
},
|
||||
"BookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24657,7 +24738,7 @@
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/BookingAttendee"
|
||||
}
|
||||
},
|
||||
"guests": {
|
||||
@@ -24814,7 +24895,7 @@
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/BookingAttendee"
|
||||
}
|
||||
},
|
||||
"guests": {
|
||||
@@ -25925,6 +26006,21 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"MarkAbsentAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"absent": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email",
|
||||
"absent"
|
||||
]
|
||||
},
|
||||
"MarkAbsentBookingInput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -25936,7 +26032,7 @@
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/MarkAbsentAttendee"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { unstable_cache } from "next/cache";
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/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 { TRPCError } from "@trpc/server";
|
||||
@@ -14,7 +15,8 @@ import { TeamsCTA } from "./CTA";
|
||||
|
||||
const getCachedTeams = unstable_cache(
|
||||
async (userId: number) => {
|
||||
return await TeamRepository.findTeamsByUserId({
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
return await teamRepo.findTeamsByUserId({
|
||||
userId,
|
||||
includeOrgs: true,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cookies, headers } from "next/headers";
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -25,7 +26,8 @@ const ServerPage = async () => {
|
||||
const session = await getServerSession({ req });
|
||||
let appStore, userAdminTeamsIds: number[];
|
||||
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) ?? [];
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeamsIds);
|
||||
} 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 SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
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(
|
||||
(t) => `${t("editing_user")}: ${user.username}`,
|
||||
@@ -37,7 +39,8 @@ const Page = async ({ params }: { params: Params }) => {
|
||||
|
||||
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();
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/video/meeting-not-started/[uid]/getServerSideProps";
|
||||
@@ -22,7 +23,8 @@ export const generateMetadata = async ({ params }: ServerPageProps) => {
|
||||
if (!parsed.success) {
|
||||
notFound();
|
||||
}
|
||||
const booking = await BookingRepository.findBookingByUid({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const booking = await bookingRepo.findBookingByUid({
|
||||
bookingUid: parsed.data.uid,
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -29,7 +30,10 @@ async function authMiddleware() {
|
||||
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) {
|
||||
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from "zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { CardComponent } from "@lib/plain/card-components";
|
||||
@@ -474,7 +475,8 @@ async function handler(request: NextRequest) {
|
||||
// Validate request body
|
||||
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) {
|
||||
return NextResponse.json({
|
||||
@@ -502,7 +504,7 @@ async function handler(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Parse user metadata
|
||||
|
||||
@@ -18,7 +18,8 @@ import { STEPS } from "~/apps/installation/[[...step]]/constants";
|
||||
import type { OnboardingPageProps, TEventTypeGroup } from "~/apps/installation/[[...step]]/step-view";
|
||||
|
||||
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) {
|
||||
return null;
|
||||
|
||||
@@ -101,7 +101,8 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
|
||||
name = profileUsername || username;
|
||||
|
||||
const [user] = await UserRepository.findUsersByUsername({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const [user] = await userRepo.findUsersByUsername({
|
||||
usernameList: [name],
|
||||
orgSlug: org,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req } = context;
|
||||
@@ -12,7 +13,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
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,
|
||||
});
|
||||
|
||||
|
||||
@@ -101,8 +101,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const enrichedBookingUser = booking.user
|
||||
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
|
||||
? await userRepo.enrichUserWithItsProfile({ user: booking.user })
|
||||
: null;
|
||||
|
||||
const eventUrl = await buildEventUrlFromBooking({
|
||||
|
||||
@@ -106,7 +106,8 @@ const checkIfUserIsHost = async ({
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req } = context;
|
||||
|
||||
const booking = await BookingRepository.findBookingForMeetingPage({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const booking = await bookingRepo.findBookingForMeetingPage({
|
||||
bookingUid: context.query.uid as string,
|
||||
});
|
||||
|
||||
@@ -140,9 +141,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
})
|
||||
: false;
|
||||
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const profile = booking.user
|
||||
? (
|
||||
await UserRepository.enrichUserWithItsProfile({
|
||||
await userRepo.enrichUserWithItsProfile({
|
||||
user: booking.user,
|
||||
})
|
||||
).profile
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const booking = await BookingRepository.findBookingForMeetingEndedPage({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const booking = await bookingRepo.findBookingForMeetingEndedPage({
|
||||
bookingUid: context.query.uid as string,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
// change the type
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const booking = await BookingRepository.findBookingByUid({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const booking = await bookingRepo.findBookingByUid({
|
||||
bookingUid: context.query.uid as string,
|
||||
});
|
||||
|
||||
|
||||
@@ -18,10 +18,6 @@ describe("Middleware - POST requests restriction", () => {
|
||||
const req1 = createRequest("/api/auth/signup", "POST");
|
||||
const res1 = checkPostMethod(req1);
|
||||
expect(res1).toBeNull();
|
||||
|
||||
const req2 = createRequest("/api/trpc/book/event", "POST");
|
||||
const res2 = checkPostMethod(req2);
|
||||
expect(res2).toBeNull();
|
||||
});
|
||||
|
||||
it("should block POST requests to not-allowed app routes", async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const POST_METHODS_ALLOWED_API_ROUTES = ["/api/auth/signup", "/api/trpc/"];
|
||||
export const POST_METHODS_ALLOWED_API_ROUTES = ["/api/auth/signup"];
|
||||
export function checkPostMethod(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
if (!POST_METHODS_ALLOWED_API_ROUTES.some((route) => pathname.startsWith(route)) && req.method === "POST") {
|
||||
@@ -69,10 +69,6 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
||||
return responseWithHeaders({ url, res: routingFormRewriteResponse, req });
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/trpc/")) {
|
||||
requestHeaders.set("x-cal-timezone", req.headers.get("x-vercel-ip-timezone") ?? "");
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/auth/signup")) {
|
||||
const isSignupDisabled = await safeGet<boolean>("isSignupDisabled");
|
||||
// If is in maintenance mode, point the url pathname to the maintenance page
|
||||
@@ -178,7 +174,6 @@ export const config = {
|
||||
"/:path*/embed",
|
||||
// API routes
|
||||
"/api/auth/signup",
|
||||
"/api/trpc/:path*",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
let rescheduledToUid: string | null = null;
|
||||
if (bookingInfo.rescheduled) {
|
||||
const rescheduledTo = await BookingRepository.findFirstBookingByReschedule({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const rescheduledTo = await bookingRepo.findFirstBookingByReschedule({
|
||||
originalBookingUid: bookingInfo.uid,
|
||||
});
|
||||
rescheduledToUid = rescheduledTo?.uid ?? null;
|
||||
@@ -89,7 +90,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
} | null = null;
|
||||
|
||||
if (bookingInfo.fromReschedule) {
|
||||
previousBooking = await BookingRepository.findReschedulerByUid({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
previousBooking = await bookingRepo.findReschedulerByUid({
|
||||
uid: bookingInfo.fromReschedule,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.4.17",
|
||||
"version": "5.4.19",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -1,455 +0,0 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { Page, Browser, Route, Response } from "@playwright/test";
|
||||
import type { Team, EventType, User } from "@prisma/client";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { CalendarCacheRepository } from "@calcom/features/calendar-cache/calendar-cache.repository";
|
||||
import { getTimeMin, getTimeMax } from "@calcom/features/calendar-cache/lib/datesForCache";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
import { bookTimeSlot, doOnOrgDomain } from "./lib/testUtils";
|
||||
|
||||
/**
|
||||
* Booking Race Condition Prevention Test
|
||||
*
|
||||
* This test validates that the calendar caching system and booking logic correctly
|
||||
* prevent double-booking race conditions that occurred in production. It serves as
|
||||
* a regression test to ensure the following mechanisms work properly:
|
||||
*
|
||||
* 1. **Race Condition Prevention**: Ensures concurrent booking requests for the same
|
||||
* time slot don't result in double bookings to the same host.
|
||||
*
|
||||
* 2. **Calendar Cache Functionality**: Validates that the calendar cache system works
|
||||
* correctly with proper cache key generation using expanded date ranges.
|
||||
*
|
||||
* 3. **Round-Robin Distribution**: Verifies that when both bookings succeed, they are
|
||||
* distributed to different hosts as expected in round-robin scheduling.
|
||||
*
|
||||
* 4. **Database Constraint Protection**: Confirms that unique constraints prevent
|
||||
* duplicate bookings when race conditions are detected.
|
||||
*
|
||||
* **Test Setup**:
|
||||
* - Creates a round-robin team with multiple hosts
|
||||
* - Populates calendar cache with deterministic availability data
|
||||
* - Mocks Google Calendar API to simulate real-world scenarios
|
||||
* - Executes concurrent booking requests at the same time slot
|
||||
*
|
||||
* **Expected Outcomes**:
|
||||
* - [200, 409]: One booking succeeds, one fails due to constraint violation (preferred)
|
||||
* - [200, 200]: Both succeed but distributed to different hosts (acceptable)
|
||||
* - [200, 200] with same host: Double booking occurred (test failure - race condition)
|
||||
*
|
||||
* **Production Context**:
|
||||
* The original race condition was caused by:
|
||||
* - Stale calendar cache showing hosts as available
|
||||
* - Microsecond timing differences in Date.valueOf() for idempotency keys
|
||||
* - Both requests selecting the same host due to identical cache state
|
||||
*
|
||||
* This test recreates similar conditions but in a controlled way to verify
|
||||
* the prevention mechanisms work correctly.
|
||||
*/
|
||||
|
||||
test.describe("Booking Race Condition Prevention", () => {
|
||||
test("Prevents double-booking race condition and validates cache functionality", async ({
|
||||
page,
|
||||
users,
|
||||
orgs,
|
||||
browser,
|
||||
}) => {
|
||||
const { org, team, teamEvent, teamMembers } = await setupTeamWithRoundRobin(users, orgs);
|
||||
|
||||
await setupGoogleCalendarCredentials(teamMembers);
|
||||
await createIdenticalBookingHistories(teamMembers, teamEvent.id);
|
||||
|
||||
const { targetHost, calendarCacheHits } = await setupCalendarCache(teamMembers);
|
||||
await enableCalendarCacheFeatures(team.id);
|
||||
|
||||
const { firstResponse, secondResponse } = await performConcurrentBookings(
|
||||
page,
|
||||
browser,
|
||||
org,
|
||||
team,
|
||||
teamEvent
|
||||
);
|
||||
|
||||
const bookingResults = await analyzeBookingResults(teamEvent.id, firstResponse, secondResponse);
|
||||
|
||||
// Validate race condition prevention
|
||||
expect(bookingResults.isRaceConditionPrevented).toBe(true);
|
||||
|
||||
// Log results for monitoring
|
||||
console.log("Race condition prevention test results:", {
|
||||
totalBookings: bookingResults.bookings.length,
|
||||
responseStatuses: bookingResults.responseStatuses,
|
||||
sameHostSelected: bookingResults.sameHostSelected,
|
||||
preventionMechanism: bookingResults.preventionMechanism,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper Functions
|
||||
|
||||
async function setupTeamWithRoundRobin(users: Fixtures["users"], orgs: Fixtures["orgs"]) {
|
||||
const org = await orgs.create({ name: "TestOrg" });
|
||||
const teamMatesObj = [{ name: "teammate-1" }, { name: "teammate-2" }];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
}
|
||||
);
|
||||
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const teamEvent = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
const teamMemberships = await prisma.membership.findMany({
|
||||
where: { teamId: team.id },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
const teamMembers = teamMemberships.map((membership) => membership.user);
|
||||
|
||||
return { org, team, teamEvent, teamMembers };
|
||||
}
|
||||
|
||||
async function setupGoogleCalendarCredentials(teamMembers: User[]) {
|
||||
// Ensure Google Calendar app exists
|
||||
const googleCalendarApp = await prisma.app.findFirst({
|
||||
where: { slug: "google-calendar" },
|
||||
});
|
||||
|
||||
if (!googleCalendarApp) {
|
||||
await prisma.app.create({
|
||||
data: {
|
||||
slug: "google-calendar",
|
||||
dirName: "google-calendar",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create credentials for each team member
|
||||
for (const member of teamMembers) {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key: {
|
||||
access_token: "test_access_token",
|
||||
refresh_token: "test_refresh_token",
|
||||
scope: "https://www.googleapis.com/auth/calendar.events",
|
||||
token_type: "Bearer",
|
||||
expiry_date: Date.now() + 3600000,
|
||||
},
|
||||
userId: member.id,
|
||||
appId: "google-calendar",
|
||||
invalid: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createIdenticalBookingHistories(teamMembers: User[], eventTypeId: number) {
|
||||
// Create identical booking histories to ensure deterministic round-robin behavior
|
||||
// This prevents random host selection from interfering with race condition testing
|
||||
const identicalTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
for (const member of teamMembers) {
|
||||
// Create multiple bookings with identical timestamps
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
uid: `test-booking-${member.id}-${i}-${Date.now()}`,
|
||||
title: `Test booking ${i}`,
|
||||
startTime: new Date(Date.now() - (i + 2) * 24 * 60 * 60 * 1000),
|
||||
endTime: new Date(Date.now() - (i + 2) * 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
|
||||
eventTypeId,
|
||||
userId: member.id,
|
||||
status: "ACCEPTED",
|
||||
createdAt: identicalTimestamp,
|
||||
updatedAt: identicalTimestamp,
|
||||
attendees: {
|
||||
create: {
|
||||
email: `test${i}@example.com`,
|
||||
name: `Test Attendee ${i}`,
|
||||
timeZone: "UTC",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupCalendarCache(teamMembers: User[]) {
|
||||
// Set up calendar cache with stale data to test cache functionality
|
||||
// This simulates the production scenario where cache shows availability
|
||||
// but real calendar API might show different data
|
||||
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
const cacheTimeRange = {
|
||||
timeMin: getTimeMin(tomorrow.toISOString()),
|
||||
timeMax: getTimeMax(tomorrow.toISOString()),
|
||||
};
|
||||
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId: { in: teamMembers.map((m) => m.id) },
|
||||
type: "google_calendar",
|
||||
},
|
||||
});
|
||||
|
||||
const calendarCacheRepo = new CalendarCacheRepository(null);
|
||||
const targetHost = teamMembers[0]; // First host should be selected by round-robin
|
||||
const calendarCacheHits: string[] = [];
|
||||
|
||||
for (let i = 0; i < credentials.length; i++) {
|
||||
const credential = credentials[i];
|
||||
const member = teamMembers[i];
|
||||
|
||||
const cacheArgs = {
|
||||
timeMin: cacheTimeRange.timeMin,
|
||||
timeMax: cacheTimeRange.timeMax,
|
||||
items: [{ id: member.email! }],
|
||||
};
|
||||
|
||||
// Cache shows target host as available, others as busy
|
||||
// This ensures deterministic host selection during concurrent requests
|
||||
const availabilityData = {
|
||||
kind: "calendar#freeBusy",
|
||||
calendars: {
|
||||
[member.email!]: {
|
||||
busy:
|
||||
member.id === targetHost.id
|
||||
? []
|
||||
: [
|
||||
{
|
||||
start: "2025-07-02T08:00:00.000Z",
|
||||
end: "2025-07-02T08:30:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await calendarCacheRepo.upsertCachedAvailability({
|
||||
credentialId: credential.id,
|
||||
userId: member.id,
|
||||
args: cacheArgs,
|
||||
value: availabilityData,
|
||||
});
|
||||
|
||||
calendarCacheHits.push(`${member.email}-${credential.id}`);
|
||||
}
|
||||
|
||||
return { targetHost, calendarCacheHits };
|
||||
}
|
||||
|
||||
async function enableCalendarCacheFeatures(teamId: number) {
|
||||
await prisma.teamFeatures.createMany({
|
||||
data: [
|
||||
{
|
||||
teamId,
|
||||
featureId: "calendar-cache",
|
||||
assignedAt: new Date(),
|
||||
assignedBy: "race-condition-test",
|
||||
},
|
||||
{
|
||||
teamId,
|
||||
featureId: "calendar-cache-serve",
|
||||
assignedAt: new Date(),
|
||||
assignedBy: "race-condition-test",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function mockGoogleCalendarAPI(page: Page) {
|
||||
// Mock Google Calendar API to simulate real-world calendar data
|
||||
// This creates a mismatch with cache data to test cache functionality
|
||||
await page.route("**/calendar/v3/freeBusy**", async (route: Route) => {
|
||||
const mockResponse = {
|
||||
kind: "calendar#freeBusy",
|
||||
calendars: {
|
||||
"pro-user@example.com": {
|
||||
busy: [
|
||||
{
|
||||
start: "2025-07-02T08:00:00.000Z",
|
||||
end: "2025-07-02T09:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
"teammate-1@example.com": {
|
||||
busy: [
|
||||
{
|
||||
start: "2025-07-02T08:00:00.000Z",
|
||||
end: "2025-07-02T09:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
"teammate-2@example.com": {
|
||||
busy: [
|
||||
{
|
||||
start: "2025-07-02T08:00:00.000Z",
|
||||
end: "2025-07-02T09:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(mockResponse),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Local type for org/team with parsed metadata
|
||||
export type TeamWithMetadata = Team & { metadata: z.infer<typeof teamMetadataSchema> };
|
||||
|
||||
async function performConcurrentBookings(
|
||||
page: Page,
|
||||
browser: Browser,
|
||||
org: TeamWithMetadata,
|
||||
team: TeamWithMetadata,
|
||||
teamEvent: EventType
|
||||
) {
|
||||
let firstResponse: Response | undefined;
|
||||
let secondResponse: Response | undefined;
|
||||
|
||||
await doOnOrgDomain({ orgSlug: org.slug, page }, async () => {
|
||||
const context1 = await browser.newContext();
|
||||
const context2 = await browser.newContext();
|
||||
const page1 = await context1.newPage();
|
||||
const page2 = await context2.newPage();
|
||||
|
||||
// Apply API mocking
|
||||
await mockGoogleCalendarAPI(page1);
|
||||
await mockGoogleCalendarAPI(page2);
|
||||
|
||||
// Navigate to booking page
|
||||
await page1.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
|
||||
await page2.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
|
||||
|
||||
// Select tomorrow's date
|
||||
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
const targetDay = tomorrow.getDate().toString();
|
||||
|
||||
await page1
|
||||
.locator(`[data-testid="day"][data-disabled="false"]`)
|
||||
.filter({ hasText: new RegExp(`^${targetDay}$`) })
|
||||
.click();
|
||||
await page2
|
||||
.locator(`[data-testid="day"][data-disabled="false"]`)
|
||||
.filter({ hasText: new RegExp(`^${targetDay}$`) })
|
||||
.click();
|
||||
|
||||
// Select first available time slot
|
||||
await page1.locator('[data-testid="time"]').nth(0).waitFor();
|
||||
await page2.locator('[data-testid="time"]').nth(0).waitFor();
|
||||
await page1.locator('[data-testid="time"]').nth(0).click();
|
||||
await page2.locator('[data-testid="time"]').nth(0).click();
|
||||
|
||||
// Set up response listeners
|
||||
const responsePromise1 = page1.waitForResponse((response) => response.url().includes("/api/book/event"));
|
||||
const responsePromise2 = page2.waitForResponse((response) => response.url().includes("/api/book/event"));
|
||||
|
||||
// Execute concurrent bookings
|
||||
const bookingPromise1 = bookTimeSlot(page1, {
|
||||
name: "Guest A",
|
||||
email: "guest-a@test.com",
|
||||
expectedStatusCode: undefined,
|
||||
});
|
||||
|
||||
const bookingPromise2 = bookTimeSlot(page2, {
|
||||
name: "Guest B",
|
||||
email: "guest-b@test.com",
|
||||
expectedStatusCode: undefined,
|
||||
});
|
||||
|
||||
// Wait for both bookings to complete
|
||||
await Promise.allSettled([bookingPromise1, bookingPromise2]);
|
||||
const [firstResponseLocal, secondResponseLocal] = await Promise.all([responsePromise1, responsePromise2]);
|
||||
|
||||
firstResponse = firstResponseLocal;
|
||||
secondResponse = secondResponseLocal;
|
||||
|
||||
// Verify success pages for successful bookings
|
||||
if (firstResponse && firstResponse.status() === 200) {
|
||||
await expect(page1.getByTestId("success-page")).toBeVisible();
|
||||
}
|
||||
if (secondResponse && secondResponse.status() === 200) {
|
||||
await expect(page2.getByTestId("success-page")).toBeVisible();
|
||||
}
|
||||
|
||||
await context1.close();
|
||||
await context2.close();
|
||||
});
|
||||
|
||||
return { firstResponse, secondResponse };
|
||||
}
|
||||
|
||||
async function analyzeBookingResults(
|
||||
eventTypeId: number,
|
||||
firstResponse: Response | undefined,
|
||||
secondResponse: Response | undefined
|
||||
) {
|
||||
if (!firstResponse || !secondResponse) {
|
||||
throw new Error("Booking response(s) missing");
|
||||
}
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
eventTypeId,
|
||||
status: "ACCEPTED",
|
||||
attendees: {
|
||||
some: {
|
||||
email: {
|
||||
in: ["guest-a@test.com", "guest-b@test.com"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
const responseStatuses = [firstResponse.status(), secondResponse.status()].sort();
|
||||
const bothSucceeded = responseStatuses.every((s) => s === 200);
|
||||
const hasConflict = responseStatuses.includes(200) && responseStatuses.includes(409);
|
||||
const sameHostSelected = bookings.length === 2 && bookings[0].userId === bookings[1].userId;
|
||||
|
||||
// Determine prevention mechanism
|
||||
const isRaceConditionPrevented = hasConflict || (bothSucceeded && !sameHostSelected);
|
||||
|
||||
let preventionMechanism = "unknown";
|
||||
if (hasConflict) {
|
||||
preventionMechanism = "unique_constraint_violation";
|
||||
} else if (bothSucceeded && !sameHostSelected) {
|
||||
preventionMechanism = "round_robin_distribution";
|
||||
} else if (bothSucceeded && sameHostSelected) {
|
||||
preventionMechanism = "none_detected";
|
||||
}
|
||||
|
||||
return {
|
||||
bookings,
|
||||
responseStatuses,
|
||||
bothSucceeded,
|
||||
hasConflict,
|
||||
sameHostSelected,
|
||||
isRaceConditionPrevented,
|
||||
preventionMechanism,
|
||||
};
|
||||
}
|
||||
@@ -60,7 +60,6 @@
|
||||
"participants": "Participants",
|
||||
"anonymous_user": "Anonymous User",
|
||||
"session_duration": "Session Duration",
|
||||
"minutes": "minutes",
|
||||
"no_meeting_sessions_found": "No meeting sessions found",
|
||||
"not_available": "Not available",
|
||||
"disable_recording_for_organizer": "Disable recording for organizer",
|
||||
|
||||
@@ -135,7 +135,8 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const usersInOrgContext = await UserRepository.findUsersByUsername({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const usersInOrgContext = await userRepo.findUsersByUsername({
|
||||
usernameList: usernames,
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType, type EventType, type User } from "@calcom/prisma/client";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
@@ -199,7 +200,9 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
};
|
||||
|
||||
export async function getUsersInOrgContext(usernameList: string[], orgSlug: string | null) {
|
||||
const usersInOrgContext = await UserRepository.findUsersByUsername({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
|
||||
const usersInOrgContext = await userRepo.findUsersByUsername({
|
||||
usernameList,
|
||||
orgSlug,
|
||||
});
|
||||
@@ -212,7 +215,7 @@ export async function getUsersInOrgContext(usernameList: string[], orgSlug: stri
|
||||
// the platform organization does not have a domain. In this case there is no org domain but also platform member
|
||||
// "User.organization" is not null so "UserRepository.findUsersByUsername" returns empty array and we do this as a last resort
|
||||
// call to find platform member.
|
||||
return await UserRepository.findPlatformMembersByUsernames({
|
||||
return await userRepo.findPlatformMembersByUsernames({
|
||||
usernameList,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19759,7 +19759,8 @@
|
||||
"OOO_CREATED",
|
||||
"AFTER_HOSTS_CAL_VIDEO_NO_SHOW",
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW",
|
||||
"FORM_SUBMITTED_NO_EVENT"
|
||||
"FORM_SUBMITTED_NO_EVENT",
|
||||
"RESERVATION_EXPIRED"
|
||||
]
|
||||
},
|
||||
"secret": {
|
||||
@@ -19826,7 +19827,8 @@
|
||||
"OOO_CREATED",
|
||||
"AFTER_HOSTS_CAL_VIDEO_NO_SHOW",
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW",
|
||||
"FORM_SUBMITTED_NO_EVENT"
|
||||
"FORM_SUBMITTED_NO_EVENT",
|
||||
"RESERVATION_EXPIRED"
|
||||
]
|
||||
},
|
||||
"secret": {
|
||||
@@ -21957,7 +21959,7 @@
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"Attendee": {
|
||||
"CreateBookingAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -22085,7 +22087,7 @@
|
||||
"description": "The attendee's details.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -22203,7 +22205,7 @@
|
||||
"description": "The attendee's details.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -22326,7 +22328,7 @@
|
||||
"description": "The attendee's details.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/CreateBookingAttendee"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -22477,6 +22479,80 @@
|
||||
},
|
||||
"required": ["id", "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"
|
||||
],
|
||||
"example": "en"
|
||||
},
|
||||
"absent": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
}
|
||||
},
|
||||
"required": ["name", "email", "timeZone", "absent"]
|
||||
},
|
||||
"BookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -22588,7 +22664,7 @@
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/BookingAttendee"
|
||||
}
|
||||
},
|
||||
"guests": {
|
||||
@@ -22737,7 +22813,7 @@
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/BookingAttendee"
|
||||
}
|
||||
},
|
||||
"guests": {
|
||||
@@ -23758,6 +23834,18 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"MarkAbsentAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"absent": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["email", "absent"]
|
||||
},
|
||||
"MarkAbsentBookingInput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23769,7 +23857,7 @@
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Attendee"
|
||||
"$ref": "#/components/schemas/MarkAbsentAttendee"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userId: number) {
|
||||
try {
|
||||
@@ -25,11 +26,13 @@ export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userI
|
||||
avatar: await resizeBase64Image(avatarUrl),
|
||||
userId,
|
||||
});
|
||||
await UserRepository.updateAvatar({ id: userId, avatarUrl: resizedAvatarUrl });
|
||||
const userRepo = new UserRepository(prisma);
|
||||
await userRepo.updateAvatar({ id: userId, avatarUrl: resizedAvatarUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
await UserRepository.updateAvatar({ id: userId, avatarUrl });
|
||||
const userRepo = new UserRepository(prisma);
|
||||
await userRepo.updateAvatar({ id: userId, avatarUrl });
|
||||
} catch (error) {
|
||||
logger.error("Error updating avatarUrl from google calendar connect", error);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export const throwIfNotHaveAdminAccessToTeam = async ({
|
||||
teamId,
|
||||
@@ -11,7 +12,8 @@ export const throwIfNotHaveAdminAccessToTeam = async ({
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
const userAdminTeams = await UserRepository.getUserAdminTeams(userId);
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const userAdminTeams = await userRepo.getUserAdminTeams({ userId });
|
||||
const teamsUserHasAdminAccessFor = userAdminTeams?.teams?.map(({ team }) => team.id) ?? [];
|
||||
const hasAdminAccessToTeam = teamsUserHasAdminAccessFor.some((id) => id === teamId);
|
||||
|
||||
|
||||
@@ -99,9 +99,10 @@ export const getServerSidePropsForSingleFormView = async function getServerSideP
|
||||
|
||||
const { UserRepository } = await import("@calcom/lib/server/repository/user");
|
||||
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const formWithUserInfoProfile = {
|
||||
...form,
|
||||
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
|
||||
user: await userRepo.enrichUserWithItsProfile({ user: form.user }),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -67,9 +67,10 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
}
|
||||
|
||||
const { UserRepository } = await import("@calcom/lib/server/repository/user");
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const formWithUserProfile = {
|
||||
...form,
|
||||
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
|
||||
user: await userRepo.enrichUserWithItsProfile({ user: form.user }),
|
||||
};
|
||||
|
||||
if (
|
||||
|
||||
@@ -83,9 +83,10 @@ async function getResponseWithFormFieldsHandler({ ctx, input }: GetResponseWithF
|
||||
}
|
||||
|
||||
const { UserRepository } = await import("@calcom/lib/server/repository/user");
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const formWithUserProfile = {
|
||||
...form,
|
||||
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
|
||||
user: await userRepo.enrichUserWithItsProfile({ user: form.user }),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -80,7 +80,8 @@ export async function getServerSession(options: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await UserRepository.enrichUserWithTheProfile({
|
||||
const userRepository = new UserRepository(prisma);
|
||||
const user = await userRepository.enrichUserWithTheProfile({
|
||||
user: userFromDb,
|
||||
upId,
|
||||
});
|
||||
|
||||
@@ -116,7 +116,8 @@ const providers: Provider[] = [
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const user = await UserRepository.findByEmailAndIncludeProfilesAndPassword({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const user = await userRepo.findByEmailAndIncludeProfilesAndPassword({
|
||||
email: credentials.email,
|
||||
});
|
||||
// Don't leak information about it being username or password that is invalid
|
||||
@@ -291,7 +292,8 @@ if (isSAMLLoginEnabled) {
|
||||
locale?: string;
|
||||
}) => {
|
||||
log.debug("BoxyHQ:profile", safeStringify({ profile }));
|
||||
const user = await UserRepository.findByEmailAndIncludeProfilesAndPassword({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const user = await userRepo.findByEmailAndIncludeProfilesAndPassword({
|
||||
email: profile.email || "",
|
||||
});
|
||||
return {
|
||||
@@ -355,9 +357,8 @@ if (isSAMLLoginEnabled) {
|
||||
|
||||
const { id, firstName, lastName } = userInfo;
|
||||
const email = userInfo.email.toLowerCase();
|
||||
let user = !email
|
||||
? undefined
|
||||
: await UserRepository.findByEmailAndIncludeProfilesAndPassword({ email });
|
||||
const userRepo = new UserRepository(prisma);
|
||||
let user = !email ? undefined : await userRepo.findByEmailAndIncludeProfilesAndPassword({ email });
|
||||
if (!user) {
|
||||
const hostedCal = Boolean(HOSTED_CAL_FEATURES);
|
||||
if (hostedCal && email) {
|
||||
@@ -373,7 +374,7 @@ if (isSAMLLoginEnabled) {
|
||||
createUsersAndConnectToOrgProps,
|
||||
org,
|
||||
});
|
||||
user = await UserRepository.findByEmailAndIncludeProfilesAndPassword({
|
||||
user = await userRepo.findByEmailAndIncludeProfilesAndPassword({
|
||||
email: email,
|
||||
});
|
||||
}
|
||||
|
||||
+73
-11
@@ -9,18 +9,30 @@ import { describe, test, expect, vi } from "vitest";
|
||||
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
|
||||
// vi.mock("@calcom/lib/server/repository/user", () => {
|
||||
// return {
|
||||
// enrichUserWithItsProfile
|
||||
// }
|
||||
// })
|
||||
vi.mock("@calcom/lib/server/repository/user", () => {
|
||||
return {
|
||||
UserRepository: vi.fn().mockImplementation(() => ({
|
||||
enrichUserWithItsProfile: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe("getAllCredentialsIncludeServiceAccountKey", () => {
|
||||
test("Get an individual's credentials", async () => {
|
||||
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
|
||||
const mockEnrichUserWithItsProfile = vi.fn().mockReturnValue({
|
||||
profile: null,
|
||||
});
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
|
||||
.getAllCredentialsIncludeServiceAccountKey;
|
||||
|
||||
@@ -62,10 +74,20 @@ describe("getAllCredentialsIncludeServiceAccountKey", () => {
|
||||
describe("If CRM is enabled on the event type", () => {
|
||||
describe("With _crm credentials", () => {
|
||||
test("For users", async () => {
|
||||
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
|
||||
const mockEnrichUserWithItsProfile = vi.fn().mockReturnValue({
|
||||
profile: null,
|
||||
});
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
|
||||
.getAllCredentialsIncludeServiceAccountKey;
|
||||
|
||||
@@ -136,10 +158,20 @@ describe("getAllCredentialsIncludeServiceAccountKey", () => {
|
||||
expect(credentials).toContainEqual(expect.objectContaining({ userId: 1, type: "salesforce_crm" }));
|
||||
});
|
||||
test("For teams", async () => {
|
||||
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
|
||||
const mockEnrichUserWithItsProfile = vi.fn().mockReturnValue({
|
||||
profile: null,
|
||||
});
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
|
||||
.getAllCredentialsIncludeServiceAccountKey;
|
||||
|
||||
@@ -203,10 +235,20 @@ describe("getAllCredentialsIncludeServiceAccountKey", () => {
|
||||
expect(credentials).toContainEqual(expect.objectContaining({ teamId: 1, type: "salesforce_crm" }));
|
||||
});
|
||||
test("For child of managed event type", async () => {
|
||||
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
|
||||
const mockEnrichUserWithItsProfile = vi.fn().mockReturnValue({
|
||||
profile: null,
|
||||
});
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
|
||||
.getAllCredentialsIncludeServiceAccountKey;
|
||||
|
||||
@@ -298,10 +340,20 @@ describe("getAllCredentialsIncludeServiceAccountKey", () => {
|
||||
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
|
||||
.getAllCredentialsIncludeServiceAccountKey;
|
||||
const orgId = 3;
|
||||
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
|
||||
const mockEnrichUserWithItsProfile = vi.fn().mockReturnValue({
|
||||
profile: { organizationId: orgId },
|
||||
});
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const crmCredential = {
|
||||
id: 1,
|
||||
type: "salesforce_crm",
|
||||
@@ -611,10 +663,20 @@ describe("getAllCredentialsIncludeServiceAccountKey", () => {
|
||||
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
|
||||
.getAllCredentialsIncludeServiceAccountKey;
|
||||
const orgId = 3;
|
||||
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
|
||||
const mockEnrichUserWithItsProfile = vi.fn().mockReturnValue({
|
||||
profile: { organizationId: orgId },
|
||||
});
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const crmCredential = {
|
||||
id: 1,
|
||||
type: "salesforce_crm",
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ export const getAllCredentialsIncludeServiceAccountKey = async (
|
||||
}
|
||||
}
|
||||
|
||||
const { profile } = await UserRepository.enrichUserWithItsProfile({
|
||||
const { profile } = await new UserRepository(prisma).enrichUserWithItsProfile({
|
||||
user: user,
|
||||
});
|
||||
|
||||
|
||||
@@ -506,7 +506,8 @@ async function handler(
|
||||
(!isConfirmedByDefault && !userReschedulingIsOwner) ||
|
||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
||||
) {
|
||||
const existingBooking = await BookingRepository.getValidBookingFromEventTypeForAttendee({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({
|
||||
eventTypeId,
|
||||
bookerEmail,
|
||||
bookerPhoneNumber,
|
||||
|
||||
@@ -116,7 +116,7 @@ export const findUsersByUsername = async ({
|
||||
usernameList: string[];
|
||||
}) => {
|
||||
log.debug("findUsersByUsername", { usernameList, orgSlug });
|
||||
const { where, profiles } = await UserRepository._getWhereClauseForFindingUsersByUsername({
|
||||
const { where, profiles } = await new UserRepository(prisma)._getWhereClauseForFindingUsersByUsername({
|
||||
orgSlug,
|
||||
usernameList,
|
||||
});
|
||||
|
||||
@@ -3,10 +3,12 @@ import type { Prisma } from "@prisma/client";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
export async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
||||
const originalBooking = await BookingRepository.findOriginalRescheduledBooking(uid, seatsEventType);
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const originalBooking = await bookingRepo.findOriginalRescheduledBooking(uid, seatsEventType);
|
||||
|
||||
if (!originalBooking) {
|
||||
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
import { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar";
|
||||
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const testUser = {
|
||||
email: "test@test.com",
|
||||
@@ -38,7 +39,7 @@ describe("deleteCredential", () => {
|
||||
test("Delete video credential", async () => {
|
||||
const handleDeleteCredential = (await import("./handleDeleteCredential")).default;
|
||||
|
||||
const user = await UserRepository.create({
|
||||
const user = await new UserRepository(prisma).create({
|
||||
...testUser,
|
||||
});
|
||||
|
||||
@@ -74,7 +75,7 @@ describe("deleteCredential", () => {
|
||||
test("Delete calendar credential", async () => {
|
||||
const handleDeleteCredential = (await import("./handleDeleteCredential")).default;
|
||||
|
||||
const user = await UserRepository.create({
|
||||
const user = await new UserRepository(prisma).create({
|
||||
...testUser,
|
||||
});
|
||||
|
||||
|
||||
@@ -388,10 +388,13 @@ describe("CreditService", () => {
|
||||
|
||||
describe("getMonthlyCredits", () => {
|
||||
it("should return 0 if subscription is not active", async () => {
|
||||
vi.mocked(TeamRepository.findTeamWithMembers).mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }],
|
||||
} as any);
|
||||
const mockTeamRepo = {
|
||||
findTeamWithMembers: vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }],
|
||||
}),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
const mockTeamBillingService = {
|
||||
getSubscriptionStatus: vi.fn().mockResolvedValue("trialing"),
|
||||
@@ -405,10 +408,13 @@ describe("CreditService", () => {
|
||||
});
|
||||
|
||||
it("should calculate credits based on active members and price", async () => {
|
||||
vi.mocked(TeamRepository.findTeamWithMembers).mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }, { accepted: true }, { accepted: true }],
|
||||
} as any);
|
||||
const mockTeamRepo = {
|
||||
findTeamWithMembers: vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }, { accepted: true }, { accepted: true }],
|
||||
}),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
const mockTeamBillingService = {
|
||||
getSubscriptionStatus: vi.fn().mockResolvedValue("active"),
|
||||
|
||||
@@ -532,7 +532,8 @@ export class CreditService {
|
||||
}
|
||||
|
||||
async getMonthlyCredits(teamId: number) {
|
||||
const team = await TeamRepository.findTeamWithMembers(teamId);
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
const team = await teamRepo.findTeamWithMembers(teamId);
|
||||
|
||||
if (!team) return 0;
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ const handleUserEvents = async (event: DirectorySyncEvent, organizationId: numbe
|
||||
|
||||
if (user) {
|
||||
if (eventData.active) {
|
||||
if (UserRepository.isAMemberOfOrganization({ user, organizationId })) {
|
||||
if (await new UserRepository(prisma).isAMemberOfOrganization({ user, organizationId })) {
|
||||
await syncCustomAttributesToUser({
|
||||
event,
|
||||
userEmail,
|
||||
|
||||
@@ -105,7 +105,7 @@ export class OrganizationPaymentService {
|
||||
|
||||
const stripeCustomerId = customer.stripeCustomerId;
|
||||
if (existingCustomer && parsedMetadata) {
|
||||
await UserRepository.updateStripeCustomerId({
|
||||
await new UserRepository(prisma).updateStripeCustomerId({
|
||||
id: existingCustomer.id,
|
||||
stripeCustomerId,
|
||||
existingMetadata: parsedMetadata,
|
||||
|
||||
@@ -317,7 +317,7 @@ async function ensureStripeCustomerIdIsUpdated({
|
||||
}) {
|
||||
const parsedMetadata = userMetadata.parse(owner.metadata);
|
||||
|
||||
await UserRepository.updateStripeCustomerId({
|
||||
await new UserRepository(prisma).updateStripeCustomerId({
|
||||
id: owner.id,
|
||||
stripeCustomerId: stripeCustomerId,
|
||||
existingMetadata: parsedMetadata,
|
||||
|
||||
@@ -279,7 +279,7 @@ export const findUserToBeOrgOwner = async (email: string) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await UserRepository.enrichUserWithItsProfile({
|
||||
return await new UserRepository(prisma).enrichUserWithItsProfile({
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import {
|
||||
getDate,
|
||||
createBookingScenario,
|
||||
@@ -242,7 +244,8 @@ describe("roundRobinManualReassignment test", () => {
|
||||
userId: fixedHost.id,
|
||||
});
|
||||
|
||||
const attendees = await BookingRepository.getBookingAttendees(123);
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const attendees = await bookingRepo.getBookingAttendees(123);
|
||||
|
||||
expect(attendees.some((attendee) => attendee.email === currentRRHost.email)).toBe(false);
|
||||
expect(attendees.some((attendee) => attendee.email === newHost.email)).toBe(true);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import {
|
||||
getDate,
|
||||
createBookingScenario,
|
||||
@@ -286,7 +288,8 @@ describe("roundRobinReassignment test", () => {
|
||||
userId: 1,
|
||||
});
|
||||
|
||||
const attendees = await BookingRepository.getBookingAttendees(123);
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const attendees = await bookingRepo.getBookingAttendees(123);
|
||||
|
||||
expect(attendees.some((attendee) => attendee.email === currentRRHost.email)).toBe(false);
|
||||
|
||||
|
||||
@@ -344,7 +344,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
{isCalVideo && !isPlatform && (
|
||||
<div className="bg-muted mt-2 space-y-2 rounded-lg p-4">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Controller
|
||||
name="calVideoSettings.disableRecordingForGuests"
|
||||
defaultValue={!!eventType.calVideoSettings?.disableRecordingForGuests}
|
||||
@@ -352,7 +352,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_guests")}
|
||||
labelClassName="text-sm"
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
@@ -367,7 +367,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_organizer")}
|
||||
labelClassName="text-sm"
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
@@ -382,7 +382,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("enable_automatic_transcription")}
|
||||
labelClassName="text-sm"
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
@@ -398,7 +398,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_transcription_for_guests")}
|
||||
labelClassName="text-sm"
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
@@ -414,7 +414,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_transcription_for_organizer")}
|
||||
labelClassName="text-sm"
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
@@ -427,7 +427,8 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
label={t("enter_redirect_url_on_exit_description")}
|
||||
defaultValue={eventType.calVideoSettings?.redirectUrlOnExit || ""}
|
||||
data-testid="calVideoSettings.redirectUrlOnExit"
|
||||
containerClassName="mt-2"
|
||||
containerClassName="mt-4"
|
||||
className="leading-6"
|
||||
{...formMethods.register("calVideoSettings.redirectUrlOnExit", {
|
||||
setValueAs: (v) => (!v || v.trim() === "" ? null : v),
|
||||
})}
|
||||
|
||||
@@ -237,7 +237,7 @@ export const getPublicEvent = async (
|
||||
const orgQuery = org ? getSlugOrRequestedSlug(org) : null;
|
||||
// In case of dynamic group event, we fetch user's data and use the default event.
|
||||
if (usernameList.length > 1) {
|
||||
const usersInOrgContext = await UserRepository.findUsersByUsername({
|
||||
const usersInOrgContext = await new UserRepository(prisma).findUsersByUsername({
|
||||
usernameList,
|
||||
orgSlug: org,
|
||||
});
|
||||
@@ -397,7 +397,7 @@ export const getPublicEvent = async (
|
||||
const usersAsHosts = event.hosts.map((host) => host.user);
|
||||
|
||||
// Enrich users in a single batch call
|
||||
const enrichedUsers = await UserRepository.enrichUsersWithTheirProfiles(usersAsHosts);
|
||||
const enrichedUsers = await new UserRepository(prisma).enrichUsersWithTheirProfiles(usersAsHosts);
|
||||
|
||||
// Map enriched users back to the hosts
|
||||
const hosts = event.hosts.map((host, index) => ({
|
||||
@@ -408,7 +408,7 @@ export const getPublicEvent = async (
|
||||
const eventWithUserProfiles = {
|
||||
...event,
|
||||
owner: event.owner
|
||||
? await UserRepository.enrichUserWithItsProfile({
|
||||
? await new UserRepository(prisma).enrichUserWithItsProfile({
|
||||
user: event.owner,
|
||||
})
|
||||
: null,
|
||||
@@ -640,7 +640,7 @@ async function getOwnerFromUsersArray(prisma: PrismaClient, eventTypeId: number)
|
||||
if (!users.length) return null;
|
||||
|
||||
// Batch enrich users in a single call
|
||||
const enrichedUsers = await UserRepository.enrichUsersWithTheirProfiles(users);
|
||||
const enrichedUsers = await new UserRepository(prisma).enrichUsersWithTheirProfiles(users);
|
||||
|
||||
// Map the enriched users back to include the organization info
|
||||
const usersWithUserProfile = enrichedUsers.map((user) => ({
|
||||
|
||||
@@ -223,7 +223,8 @@ const getWebhooksService = async (bookingUid: string, platformClientId?: string)
|
||||
const assertCanAccessBooking = async (bookingUid: string, userId?: number) => {
|
||||
if (!userId) throw new HttpError({ statusCode: 401 });
|
||||
|
||||
const booking = await BookingRepository.findBookingByUidAndUserId({ bookingUid, userId });
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const booking = await bookingRepo.findBookingByUidAndUserId({ bookingUid, userId });
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({ statusCode: 403, message: "You are not allowed to access this booking" });
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export class RoleManagementError extends Error {
|
||||
constructor(message: string, public readonly code: RoleManagementErrorCode) {
|
||||
super(message);
|
||||
this.name = "RoleManagementError";
|
||||
}
|
||||
}
|
||||
|
||||
export enum RoleManagementErrorCode {
|
||||
UNAUTHORIZED = "UNAUTHORIZED",
|
||||
ROLE_NOT_FOUND = "ROLE_NOT_FOUND",
|
||||
INVALID_ROLE = "INVALID_ROLE",
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { RoleType } from "@calcom/kysely/types";
|
||||
|
||||
import type { Role, RolePermission, RoleType as DomainRoleType } from "../models/Role";
|
||||
|
||||
type KyselyRole = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
description: string | null;
|
||||
teamId: number | null;
|
||||
type: RoleType;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
permissions: Array<{
|
||||
id: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export class RoleMapper {
|
||||
static toDomain(kyselyRole: KyselyRole): Role {
|
||||
return {
|
||||
id: kyselyRole.id,
|
||||
name: kyselyRole.name,
|
||||
description: kyselyRole.description || undefined,
|
||||
color: kyselyRole.color || undefined,
|
||||
teamId: kyselyRole.teamId || undefined,
|
||||
type: kyselyRole.type as DomainRoleType,
|
||||
permissions: kyselyRole.permissions.map((p) => ({
|
||||
id: p.id,
|
||||
resource: p.resource,
|
||||
action: p.action,
|
||||
})),
|
||||
createdAt: kyselyRole.createdAt,
|
||||
updatedAt: kyselyRole.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
static toKysely(role: Role): Omit<KyselyRole, "permissions"> {
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description || null,
|
||||
color: role.color || null,
|
||||
teamId: role.teamId || null,
|
||||
type: role.type as DomainRoleType,
|
||||
createdAt: role.createdAt,
|
||||
updatedAt: role.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
static permissionToKysely(roleId: string, permission: RolePermission) {
|
||||
return {
|
||||
roleId,
|
||||
resource: permission.resource,
|
||||
action: permission.action,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@ export type RoleType = (typeof RoleType)[keyof typeof RoleType];
|
||||
|
||||
export interface RolePermission {
|
||||
id: string;
|
||||
roleId: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
@@ -43,3 +45,8 @@ export interface UpdateRolePermissionsData {
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type PermissionChange = {
|
||||
resource: string;
|
||||
action: string;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import type { Transaction } from "kysely";
|
||||
|
||||
import type { DB } from "@calcom/kysely";
|
||||
|
||||
import type { Role, CreateRoleData } from "../models/Role";
|
||||
import type { PermissionString } from "../types/permission-registry";
|
||||
import type { Role, CreateRoleData, RolePermission, PermissionChange } from "../models/Role";
|
||||
|
||||
export interface IRoleRepository {
|
||||
findByName(name: string, teamId?: number): Promise<Role | null>;
|
||||
@@ -14,12 +9,15 @@ export interface IRoleRepository {
|
||||
delete(id: string): Promise<void>;
|
||||
update(
|
||||
roleId: string,
|
||||
permissions: PermissionString[],
|
||||
permissionChanges: {
|
||||
toAdd: PermissionChange[];
|
||||
toRemove: PermissionChange[];
|
||||
},
|
||||
updates?: {
|
||||
color?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
): Promise<Role>;
|
||||
transaction<T>(callback: (repository: IRoleRepository, trx: Transaction<DB>) => Promise<T>): Promise<T>;
|
||||
getPermissions(roleId: string): Promise<RolePermission[]>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Role as PrismaRole, RolePermission as PrismaRolePermission } from "@calcom/prisma/client";
|
||||
|
||||
import type { Role, RolePermission } from "../../domain/models/Role";
|
||||
|
||||
export class RoleOutputMapper {
|
||||
static toDomain(prismaRole: PrismaRole & { permissions: PrismaRolePermission[] }): Role {
|
||||
return {
|
||||
id: prismaRole.id,
|
||||
name: prismaRole.name,
|
||||
color: prismaRole.color || undefined,
|
||||
description: prismaRole.description || undefined,
|
||||
teamId: prismaRole.teamId || undefined,
|
||||
type: prismaRole.type,
|
||||
permissions: prismaRole.permissions.map(this.toDomainPermission),
|
||||
createdAt: prismaRole.createdAt,
|
||||
updatedAt: prismaRole.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
static toDomainPermission(prismaPermission: PrismaRolePermission): RolePermission {
|
||||
return {
|
||||
id: prismaPermission.id,
|
||||
roleId: prismaPermission.roleId,
|
||||
resource: prismaPermission.resource,
|
||||
action: prismaPermission.action,
|
||||
createdAt: prismaPermission.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
static toDomainList(prismaRoles: (PrismaRole & { permissions: PrismaRolePermission[] })[]): Role[] {
|
||||
return prismaRoles.map(this.toDomain);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import kysely from "@calcom/kysely";
|
||||
import db from "@calcom/prisma";
|
||||
import type { PrismaClient as PrismaClientWithExtensions } from "@calcom/prisma";
|
||||
|
||||
import { PermissionMapper } from "../../domain/mappers/PermissionMapper";
|
||||
import type { TeamPermissions } from "../../domain/models/Permission";
|
||||
@@ -7,114 +8,101 @@ import type { CrudAction, CustomAction } from "../../domain/types/permission-reg
|
||||
import { Resource, type PermissionString } from "../../domain/types/permission-registry";
|
||||
|
||||
export class PermissionRepository implements IPermissionRepository {
|
||||
private client: PrismaClientWithExtensions;
|
||||
|
||||
constructor(client: PrismaClientWithExtensions = db) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async getUserMemberships(userId: number): Promise<TeamPermissions[]> {
|
||||
const memberships = await kysely
|
||||
.selectFrom("Membership")
|
||||
.innerJoin("Role", "Role.id", "Membership.customRoleId")
|
||||
.leftJoin("RolePermission", "RolePermission.roleId", "Role.id")
|
||||
.select(["Membership.teamId", "Role.id as roleId", "RolePermission.resource", "RolePermission.action"])
|
||||
.where("Membership.userId", "=", userId)
|
||||
.execute();
|
||||
|
||||
// Group permissions by teamId and roleId
|
||||
const membershipsWithPermissions = memberships.reduce((acc, membership) => {
|
||||
const key = `${membership.teamId}-${membership.roleId}`;
|
||||
if (!acc[key]) {
|
||||
acc[key] = {
|
||||
teamId: membership.teamId,
|
||||
role: {
|
||||
id: membership.roleId,
|
||||
permissions: [],
|
||||
const memberships = await this.client.membership.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
customRole: {
|
||||
include: {
|
||||
permissions: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (membership.resource && membership.action) {
|
||||
acc[key].role.permissions.push({
|
||||
resource: membership.resource,
|
||||
action: membership.action,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
return PermissionMapper.toDomain(Object.values(membershipsWithPermissions));
|
||||
},
|
||||
},
|
||||
});
|
||||
// Map to expected structure for PermissionMapper
|
||||
const mapped = memberships.map((membership) => ({
|
||||
teamId: membership.teamId,
|
||||
role: membership.customRole
|
||||
? {
|
||||
id: membership.customRole.id,
|
||||
permissions: membership.customRole.permissions,
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
return PermissionMapper.toDomain(mapped);
|
||||
}
|
||||
|
||||
async getMembershipByMembershipId(membershipId: number) {
|
||||
const result = await kysely
|
||||
.selectFrom("Membership")
|
||||
.leftJoin("Team", "Team.id", "Membership.teamId")
|
||||
.select([
|
||||
"Membership.id",
|
||||
"Membership.teamId",
|
||||
"Membership.userId",
|
||||
"Membership.customRoleId",
|
||||
"Team.parentId as team_parentId",
|
||||
])
|
||||
.where("Membership.id", "=", membershipId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
return {
|
||||
...result,
|
||||
team_parentId: result.team_parentId || undefined,
|
||||
};
|
||||
return this.client.membership.findUnique({
|
||||
where: { id: membershipId },
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
userId: true,
|
||||
customRoleId: true,
|
||||
team: {
|
||||
select: {
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMembershipByUserAndTeam(userId: number, teamId: number) {
|
||||
const result = await kysely
|
||||
.selectFrom("Membership")
|
||||
.leftJoin("Team", "Team.id", "Membership.teamId")
|
||||
.select([
|
||||
"Membership.id",
|
||||
"Membership.teamId",
|
||||
"Membership.userId",
|
||||
"Membership.customRoleId",
|
||||
"Team.parentId as team_parentId",
|
||||
])
|
||||
.where("Membership.userId", "=", userId)
|
||||
.where("Membership.teamId", "=", teamId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
return {
|
||||
...result,
|
||||
team_parentId: result.team_parentId || undefined,
|
||||
};
|
||||
return this.client.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
userId: true,
|
||||
customRoleId: true,
|
||||
team: {
|
||||
select: {
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOrgMembership(userId: number, orgId: number) {
|
||||
const result = await kysely
|
||||
.selectFrom("Membership")
|
||||
.select(["id", "teamId", "userId", "customRoleId"])
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", orgId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return result || null;
|
||||
return this.client.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId: orgId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
userId: true,
|
||||
customRoleId: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async checkRolePermission(roleId: string, permission: PermissionString): Promise<boolean> {
|
||||
const [resource, action] = permission.split(".");
|
||||
const hasPermission = await kysely
|
||||
.selectFrom("RolePermission")
|
||||
.select("id")
|
||||
.where("roleId", "=", roleId)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
// Global wildcard
|
||||
eb.and([eb("resource", "=", "*"), eb("action", "=", "*")]),
|
||||
// Resource wildcard
|
||||
eb.and([eb("resource", "=", "*"), eb("action", "=", action)]),
|
||||
// Action wildcard
|
||||
eb.and([eb("resource", "=", resource), eb("action", "=", "*")]),
|
||||
// Exact match
|
||||
eb.and([eb("resource", "=", resource), eb("action", "=", action)]),
|
||||
])
|
||||
)
|
||||
.executeTakeFirst();
|
||||
const hasPermission = await this.client.rolePermission.findFirst({
|
||||
where: {
|
||||
roleId,
|
||||
OR: [
|
||||
{ resource: "*", action: "*" },
|
||||
{ resource: "*", action },
|
||||
{ resource, action: "*" },
|
||||
{ resource, action },
|
||||
],
|
||||
},
|
||||
});
|
||||
return !!hasPermission;
|
||||
}
|
||||
|
||||
@@ -123,42 +111,38 @@ export class PermissionRepository implements IPermissionRepository {
|
||||
const [resource, action] = p.split(".");
|
||||
return { resource, action };
|
||||
});
|
||||
const resourceActions = permissionPairs.map((p) => [p.resource, p.action]);
|
||||
const resources = permissionPairs.map((p) => p.resource);
|
||||
const actions = permissionPairs.map((p) => p.action);
|
||||
|
||||
const matchingPermissionsCount = await kysely
|
||||
.selectFrom("RolePermission")
|
||||
.select((eb) => eb.fn.countAll().as("count"))
|
||||
.where("roleId", "=", roleId)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
// Global wildcard
|
||||
eb.and([eb("resource", "=", "*"), eb("action", "=", "*")]),
|
||||
// Resource wildcards
|
||||
eb.and([
|
||||
eb("resource", "=", "*"),
|
||||
eb(
|
||||
"action",
|
||||
"in",
|
||||
permissionPairs.map((p) => p.action)
|
||||
),
|
||||
]),
|
||||
// Action wildcards
|
||||
eb.and([
|
||||
eb(
|
||||
"resource",
|
||||
"in",
|
||||
permissionPairs.map((p) => p.resource)
|
||||
),
|
||||
eb("action", "=", "*"),
|
||||
]),
|
||||
// Exact matches
|
||||
eb.or(
|
||||
permissionPairs.map((p) => eb.and([eb("resource", "=", p.resource), eb("action", "=", p.action)]))
|
||||
),
|
||||
])
|
||||
const matchingPermissions = await this.client.$queryRaw<[{ count: bigint }]>`
|
||||
WITH permission_checks AS (
|
||||
-- Universal permission (*,*)
|
||||
SELECT 1 as match FROM "RolePermission"
|
||||
WHERE "roleId" = ${roleId} AND "resource" = '*' AND "action" = '*'
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Wildcard resource with specific actions
|
||||
SELECT 1 as match FROM "RolePermission"
|
||||
WHERE "roleId" = ${roleId} AND "resource" = '*' AND "action" = ANY(${actions})
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Specific resources with wildcard action
|
||||
SELECT 1 as match FROM "RolePermission"
|
||||
WHERE "roleId" = ${roleId} AND "action" = '*' AND "resource" = ANY(${resources})
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Exact resource-action pairs
|
||||
SELECT 1 as match FROM "RolePermission"
|
||||
WHERE "roleId" = ${roleId} AND ("resource", "action") = ANY(${resourceActions})
|
||||
)
|
||||
.executeTakeFirstOrThrow();
|
||||
SELECT COUNT(*) as count FROM permission_checks
|
||||
`;
|
||||
|
||||
return Number(matchingPermissionsCount.count) >= permissions.length;
|
||||
return Number(matchingPermissions[0].count) >= permissions.length;
|
||||
}
|
||||
|
||||
async getResourcePermissions(
|
||||
@@ -167,22 +151,26 @@ export class PermissionRepository implements IPermissionRepository {
|
||||
resource: Resource
|
||||
): Promise<(CrudAction | CustomAction)[]> {
|
||||
// Get team-level permissions
|
||||
const membership = await kysely
|
||||
.selectFrom("Membership")
|
||||
.select("customRoleId")
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", teamId)
|
||||
.executeTakeFirst();
|
||||
|
||||
const membership = await this.client.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
select: {
|
||||
customRoleId: true,
|
||||
},
|
||||
});
|
||||
if (!membership?.customRoleId) return [];
|
||||
|
||||
const teamPermissions = await kysely
|
||||
.selectFrom("RolePermission")
|
||||
.select(["action", "resource"])
|
||||
.where("roleId", "=", membership.customRoleId)
|
||||
.where((eb) => eb.or([eb("resource", "=", resource), eb("resource", "=", Resource.All)]))
|
||||
.execute();
|
||||
|
||||
const teamPermissions = await this.client.rolePermission.findMany({
|
||||
where: {
|
||||
roleId: membership.customRoleId,
|
||||
OR: [{ resource }, { resource: Resource.All }],
|
||||
},
|
||||
select: {
|
||||
action: true,
|
||||
resource: true,
|
||||
},
|
||||
});
|
||||
return teamPermissions.map((p) => p.action as CrudAction | CustomAction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,208 +1,149 @@
|
||||
import type { Transaction } from "kysely";
|
||||
import { jsonArrayFrom } from "kysely/helpers/postgres";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import type { DB } from "@calcom/kysely";
|
||||
import kysely from "@calcom/kysely";
|
||||
import type { PrismaClient as PrismaWithExtensions } from "@calcom/prisma";
|
||||
import db from "@calcom/prisma";
|
||||
|
||||
import { RoleMapper } from "../../domain/mappers/RoleMapper";
|
||||
import type { Role, RolePermission, PermissionChange, CreateRoleData } from "../../domain/models/Role";
|
||||
import { RoleType } from "../../domain/models/Role";
|
||||
import type { CreateRoleData } from "../../domain/models/Role";
|
||||
import type { IRoleRepository } from "../../domain/repositories/IRoleRepository";
|
||||
import type { PermissionString } from "../../domain/types/permission-registry";
|
||||
import { RoleOutputMapper } from "../mappers/RoleOutputMapper";
|
||||
|
||||
type KyselyRole = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
description: string | null;
|
||||
teamId: number | null;
|
||||
type: RoleType;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
permissions: Array<{
|
||||
id: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
}>;
|
||||
};
|
||||
export class RoleRepository {
|
||||
constructor(private readonly client: PrismaWithExtensions = db) {}
|
||||
|
||||
export class RoleRepository implements IRoleRepository {
|
||||
private getRoleSelect() {
|
||||
return [
|
||||
"Role.id",
|
||||
"Role.name",
|
||||
"Role.description",
|
||||
"Role.teamId",
|
||||
"Role.type",
|
||||
"Role.color",
|
||||
"Role.createdAt",
|
||||
"Role.updatedAt",
|
||||
(eb: any) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("RolePermission")
|
||||
.select(["id", "resource", "action"])
|
||||
.whereRef("RolePermission.roleId", "=", "Role.id")
|
||||
).as("permissions"),
|
||||
] as const;
|
||||
async findByName(name: string, teamId?: number): Promise<Role | null> {
|
||||
const role = await this.client.role.findFirst({
|
||||
where: { name, teamId: teamId ?? null },
|
||||
include: { permissions: true },
|
||||
});
|
||||
return role ? RoleOutputMapper.toDomain(role) : null;
|
||||
}
|
||||
|
||||
async findByName(name: string, teamId?: number) {
|
||||
const role = await kysely
|
||||
.selectFrom("Role")
|
||||
.select(this.getRoleSelect())
|
||||
.where("name", "=", name)
|
||||
.where((eb) => (teamId ? eb("teamId", "=", teamId) : eb("teamId", "is", null)))
|
||||
.executeTakeFirst();
|
||||
|
||||
return role ? RoleMapper.toDomain(role as KyselyRole) : null;
|
||||
async findByTeamId(teamId: number): Promise<Role[]> {
|
||||
const roles = await this.client.role.findMany({
|
||||
where: { teamId },
|
||||
include: { permissions: true },
|
||||
});
|
||||
return RoleOutputMapper.toDomainList(roles);
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
const role = await kysely
|
||||
.selectFrom("Role")
|
||||
.select(this.getRoleSelect())
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst();
|
||||
|
||||
return role ? RoleMapper.toDomain(role as KyselyRole) : null;
|
||||
async roleBelongsToTeam(roleId: string, teamId: number): Promise<boolean> {
|
||||
const role = await this.client.role.findUnique({
|
||||
where: { id: roleId },
|
||||
select: { teamId: true },
|
||||
});
|
||||
return role?.teamId === teamId;
|
||||
}
|
||||
|
||||
async findByTeamId(teamId: number) {
|
||||
const roles = await kysely
|
||||
.selectFrom("Role")
|
||||
.select(this.getRoleSelect())
|
||||
.where((eb) => eb.or([eb("Role.teamId", "=", teamId), eb("Role.type", "=", RoleType.SYSTEM)]))
|
||||
.execute();
|
||||
|
||||
return roles.map((role) => RoleMapper.toDomain(role as KyselyRole));
|
||||
}
|
||||
|
||||
async create(data: CreateRoleData) {
|
||||
const roleId = uuidv4();
|
||||
|
||||
await kysely.transaction().execute(async (trx) => {
|
||||
// Create role
|
||||
await trx
|
||||
.insertInto("Role")
|
||||
.values({
|
||||
id: roleId,
|
||||
async create(data: CreateRoleData): Promise<Role> {
|
||||
const roleId = await this.client.$transaction(async (trx) => {
|
||||
const role = await trx.role.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
name: data.name,
|
||||
description: data.description || null,
|
||||
teamId: data.teamId || null,
|
||||
type: data.type || RoleType.CUSTOM,
|
||||
color: data.color || null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.execute();
|
||||
color: data.color ?? null,
|
||||
description: data.description ?? null,
|
||||
teamId: data.teamId ?? null,
|
||||
type: data.type ?? RoleType.CUSTOM,
|
||||
},
|
||||
});
|
||||
|
||||
// Create permissions only if there are any
|
||||
if (data.permissions.length > 0) {
|
||||
const permissionData = data.permissions.map((permission) => {
|
||||
const [resource, action] = permission.split(".");
|
||||
return {
|
||||
id: uuidv4(),
|
||||
roleId,
|
||||
roleId: role.id,
|
||||
resource,
|
||||
action,
|
||||
};
|
||||
});
|
||||
|
||||
await trx.insertInto("RolePermission").values(permissionData).execute();
|
||||
await trx.rolePermission.createMany({ data: permissionData });
|
||||
}
|
||||
|
||||
return roleId;
|
||||
return role.id;
|
||||
});
|
||||
|
||||
// Fetch complete role with permissions
|
||||
const completeRole = await this.findById(roleId);
|
||||
|
||||
// This should never happen
|
||||
if (!completeRole) throw new Error("Failed to create role");
|
||||
|
||||
return completeRole;
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
await kysely.transaction().execute(async (trx) => {
|
||||
await trx.deleteFrom("RolePermission").where("roleId", "=", id).execute();
|
||||
await trx.deleteFrom("Role").where("id", "=", id).execute();
|
||||
});
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.client.$transaction([
|
||||
this.client.rolePermission.deleteMany({ where: { roleId: id } }),
|
||||
this.client.role.delete({ where: { id } }),
|
||||
]);
|
||||
}
|
||||
|
||||
async update(
|
||||
roleId: string,
|
||||
permissions: PermissionString[],
|
||||
permissionChanges: {
|
||||
toAdd: PermissionChange[];
|
||||
toRemove: PermissionChange[];
|
||||
},
|
||||
updates?: {
|
||||
color?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
) {
|
||||
await kysely.transaction().execute(async (trx) => {
|
||||
await this.client.$transaction(async (trx) => {
|
||||
// Update role metadata if provided
|
||||
if (updates) {
|
||||
const updateData: Partial<Pick<KyselyRole, "name" | "color" | "description">> = {};
|
||||
|
||||
if (updates.color !== undefined) {
|
||||
updateData.color = updates.color || null;
|
||||
}
|
||||
if (updates.name !== undefined) {
|
||||
updateData.name = updates.name;
|
||||
}
|
||||
if (updates.description !== undefined) {
|
||||
updateData.description = updates.description || null;
|
||||
}
|
||||
|
||||
await trx.updateTable("Role").set(updateData).where("id", "=", roleId).execute();
|
||||
const updateData: Record<string, any> = {};
|
||||
if (updates.color !== undefined) updateData.color = updates.color || null;
|
||||
if (updates.name !== undefined) updateData.name = updates.name;
|
||||
if (updates.description !== undefined) updateData.description = updates.description || null;
|
||||
await trx.role.update({ where: { id: roleId }, data: updateData });
|
||||
}
|
||||
|
||||
// Delete existing permissions
|
||||
await trx.deleteFrom("RolePermission").where("roleId", "=", roleId).execute();
|
||||
// Remove permissions that are no longer needed
|
||||
if (permissionChanges.toRemove.length > 0) {
|
||||
await trx.rolePermission.deleteMany({
|
||||
where: {
|
||||
roleId,
|
||||
AND: permissionChanges.toRemove.map((p) => ({
|
||||
resource: p.resource,
|
||||
action: p.action,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create new permissions
|
||||
const permissionData = permissions.map((permission) => {
|
||||
const [resource, action] = permission.split(".");
|
||||
return {
|
||||
// Add new permissions
|
||||
if (permissionChanges.toAdd.length > 0) {
|
||||
const permissionData = permissionChanges.toAdd.map((permission) => ({
|
||||
id: uuidv4(),
|
||||
roleId,
|
||||
resource,
|
||||
action,
|
||||
};
|
||||
});
|
||||
resource: permission.resource,
|
||||
action: permission.action,
|
||||
}));
|
||||
|
||||
await trx.insertInto("RolePermission").values(permissionData).execute();
|
||||
|
||||
return roleId;
|
||||
await trx.rolePermission.createMany({ data: permissionData });
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch updated role
|
||||
const updatedRole = await kysely
|
||||
.selectFrom("Role")
|
||||
.select(this.getRoleSelect())
|
||||
.where("id", "=", roleId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!updatedRole) throw new Error("Failed to update role permissions");
|
||||
|
||||
return RoleMapper.toDomain(updatedRole as KyselyRole);
|
||||
// Fetch complete role with permissions
|
||||
const completeRole = await this.findById(roleId);
|
||||
if (!completeRole) throw new Error("Failed to update role");
|
||||
return completeRole;
|
||||
}
|
||||
|
||||
async transaction<T>(
|
||||
callback: (repository: IRoleRepository, trx: Transaction<DB>) => Promise<T>
|
||||
): Promise<T> {
|
||||
return kysely.transaction().execute(async (trx) => {
|
||||
// Create a new repository instance with the transaction connection
|
||||
const transactionRepo = new RoleRepository();
|
||||
// Pass both the repository and the transaction connection
|
||||
return callback(transactionRepo, trx);
|
||||
async findById(roleId: string): Promise<Role | null> {
|
||||
const role = await this.client.role.findUnique({
|
||||
where: { id: roleId },
|
||||
include: {
|
||||
permissions: true,
|
||||
},
|
||||
});
|
||||
return role ? RoleOutputMapper.toDomain(role) : null;
|
||||
}
|
||||
|
||||
async roleBelongsToTeam(roleId: string, teamId: number) {
|
||||
const role = await this.findById(roleId);
|
||||
return role?.teamId === teamId;
|
||||
async getPermissions(roleId: string): Promise<RolePermission[]> {
|
||||
const permissions = await this.client.rolePermission.findMany({
|
||||
where: { roleId },
|
||||
});
|
||||
return permissions.map(RoleOutputMapper.toDomainPermission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { RolePermission } from "@prisma/client";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { PermissionDiffService } from "../permission-diff.service";
|
||||
|
||||
describe("PermissionDiffService", () => {
|
||||
const service = new PermissionDiffService();
|
||||
|
||||
describe("calculateDiff", () => {
|
||||
it("should calculate permissions to add and remove", () => {
|
||||
const existingPermissions = [
|
||||
{ id: "1", roleId: "role1", resource: "booking", action: "create" },
|
||||
{ id: "2", roleId: "role1", resource: "booking", action: "read" },
|
||||
] as RolePermission[];
|
||||
|
||||
const newPermissions = ["booking.read", "booking.update", "event.create"];
|
||||
|
||||
const result = service.calculateDiff(newPermissions, existingPermissions);
|
||||
|
||||
expect(result.toAdd).toEqual([
|
||||
{ resource: "booking", action: "update" },
|
||||
{ resource: "event", action: "create" },
|
||||
]);
|
||||
|
||||
expect(result.toRemove).toEqual([{ id: "1", roleId: "role1", resource: "booking", action: "create" }]);
|
||||
});
|
||||
|
||||
it("should handle empty new permissions", () => {
|
||||
const existingPermissions = [
|
||||
{ id: "1", roleId: "role1", resource: "booking", action: "create" },
|
||||
] as RolePermission[];
|
||||
|
||||
const result = service.calculateDiff([], existingPermissions);
|
||||
|
||||
expect(result.toAdd).toEqual([]);
|
||||
expect(result.toRemove).toEqual(existingPermissions);
|
||||
});
|
||||
|
||||
it("should handle empty existing permissions", () => {
|
||||
const newPermissions = ["booking.create", "booking.read"];
|
||||
|
||||
const result = service.calculateDiff(newPermissions, []);
|
||||
|
||||
expect(result.toAdd).toEqual([
|
||||
{ resource: "booking", action: "create" },
|
||||
{ resource: "booking", action: "read" },
|
||||
]);
|
||||
expect(result.toRemove).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out internal _resource permissions", () => {
|
||||
const existingPermissions = [
|
||||
{ id: "1", roleId: "role1", resource: "booking", action: "create" },
|
||||
] as RolePermission[];
|
||||
|
||||
const newPermissions = ["booking.create", "booking._resource", "event.create"];
|
||||
|
||||
const result = service.calculateDiff(newPermissions, existingPermissions);
|
||||
|
||||
expect(result.toAdd).toEqual([{ resource: "event", action: "create" }]);
|
||||
expect(result.toRemove).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle identical permissions with no changes needed", () => {
|
||||
const existingPermissions = [
|
||||
{ id: "1", roleId: "role1", resource: "booking", action: "create" },
|
||||
{ id: "2", roleId: "role1", resource: "booking", action: "read" },
|
||||
] as RolePermission[];
|
||||
|
||||
const newPermissions = ["booking.create", "booking.read"];
|
||||
|
||||
const result = service.calculateDiff(newPermissions, existingPermissions);
|
||||
|
||||
expect(result.toAdd).toEqual([]);
|
||||
expect(result.toRemove).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,7 @@ import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { RoleManagementError, RoleManagementErrorCode } from "../../domain/errors/role-management.error";
|
||||
import { DEFAULT_ROLE_IDS } from "../../lib/constants";
|
||||
import { PermissionCheckService } from "../permission-check.service";
|
||||
import { RoleManagementFactory } from "../role-management.factory";
|
||||
@@ -117,7 +116,12 @@ describe("RoleManagementFactory", () => {
|
||||
it("should throw UNAUTHORIZED when user lacks permission", async () => {
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(TRPCError);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
||||
new RoleManagementError(
|
||||
"You do not have permission to change roles",
|
||||
RoleManagementErrorCode.UNAUTHORIZED
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,14 +148,16 @@ describe("RoleManagementFactory", () => {
|
||||
expect(mockRoleService.assignRoleToMember).toHaveBeenCalledWith(customRoleId, membershipId);
|
||||
});
|
||||
|
||||
it("should throw UNAUTHORIZED for invalid custom role", async () => {
|
||||
it("should throw INVALID_ROLE for invalid custom role", async () => {
|
||||
const customRoleId = "invalid-role";
|
||||
mockRoleService.roleBelongsToTeam.mockResolvedValue(false);
|
||||
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(
|
||||
manager.assignRole(userId, organizationId, customRoleId as MembershipRole, membershipId)
|
||||
).rejects.toThrow(TRPCError);
|
||||
).rejects.toThrow(
|
||||
new RoleManagementError("You do not have access to this role", RoleManagementErrorCode.INVALID_ROLE)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -159,13 +165,33 @@ describe("RoleManagementFactory", () => {
|
||||
describe("LegacyRoleManager", () => {
|
||||
beforeEach(() => {
|
||||
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(false);
|
||||
vi.mocked(prisma.membership.update).mockResolvedValue({});
|
||||
vi.mocked(prisma.membership.update).mockResolvedValue({
|
||||
id: 1,
|
||||
teamId: organizationId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: MembershipRole.MEMBER,
|
||||
userId: userId,
|
||||
disableImpersonation: false,
|
||||
accepted: true,
|
||||
customRoleId: null,
|
||||
});
|
||||
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe("checkPermissionToChangeRole", () => {
|
||||
it("should allow role change when user is owner", async () => {
|
||||
vi.mocked(isOrganisationAdmin).mockResolvedValue(true);
|
||||
vi.mocked(isOrganisationAdmin).mockResolvedValue({
|
||||
id: 1,
|
||||
teamId: organizationId,
|
||||
userId: userId,
|
||||
role: MembershipRole.OWNER,
|
||||
accepted: true,
|
||||
disableImpersonation: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
customRoleId: null,
|
||||
});
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
|
||||
});
|
||||
@@ -173,7 +199,12 @@ describe("RoleManagementFactory", () => {
|
||||
it("should throw UNAUTHORIZED when user is not owner", async () => {
|
||||
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(TRPCError);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
||||
new RoleManagementError(
|
||||
"Only owners or admin can update roles",
|
||||
RoleManagementErrorCode.UNAUTHORIZED
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,9 +5,19 @@ import { RoleType } from "@calcom/prisma/enums";
|
||||
import type { Role } from "../../domain/models/Role";
|
||||
import type { IRoleRepository } from "../../domain/repositories/IRoleRepository";
|
||||
import type { PermissionString } from "../../domain/types/permission-registry";
|
||||
import type { PermissionDiffService } from "../permission-diff.service";
|
||||
import { RoleService } from "../role.service";
|
||||
|
||||
vi.mock("../../infrastructure/repositories/RoleRepository");
|
||||
vi.mock("../permission-diff.service");
|
||||
|
||||
// Mock db.$transaction
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
default: {
|
||||
$transaction: vi.fn((cb) => cb({ membership: { update: vi.fn() } })),
|
||||
membership: { update: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
type MockRepository = {
|
||||
[K in keyof IRoleRepository]: Mock;
|
||||
@@ -16,6 +26,7 @@ type MockRepository = {
|
||||
describe("RoleService", () => {
|
||||
let service: RoleService;
|
||||
let mockRepository: MockRepository;
|
||||
let mockPermissionDiffService: { calculateDiff: Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -26,10 +37,19 @@ describe("RoleService", () => {
|
||||
create: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
update: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
roleBelongsToTeam: vi.fn(),
|
||||
getPermissions: vi.fn(),
|
||||
};
|
||||
service = new RoleService(mockRepository);
|
||||
|
||||
mockPermissionDiffService = {
|
||||
calculateDiff: vi.fn(),
|
||||
};
|
||||
|
||||
service = new RoleService(
|
||||
mockRepository,
|
||||
undefined, // permissionService is not mocked as it's not relevant for these tests
|
||||
mockPermissionDiffService as unknown as PermissionDiffService
|
||||
);
|
||||
});
|
||||
|
||||
describe("createRole", () => {
|
||||
@@ -153,9 +173,18 @@ describe("RoleService", () => {
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
it("should update role permissions", async () => {
|
||||
it("should update role permissions using permission diff", async () => {
|
||||
const roleId = "role-id";
|
||||
const permissions = ["eventType.create", "eventType.read"] as PermissionString[];
|
||||
const existingPermissions = [{ id: "1", roleId, resource: "eventType", action: "delete" }];
|
||||
|
||||
const permissionChanges = {
|
||||
toAdd: [
|
||||
{ resource: "eventType", action: "create" },
|
||||
{ resource: "eventType", action: "read" },
|
||||
],
|
||||
toRemove: [{ id: "1", roleId, resource: "eventType", action: "delete" }],
|
||||
};
|
||||
|
||||
const role: Role = {
|
||||
id: roleId,
|
||||
@@ -172,14 +201,18 @@ describe("RoleService", () => {
|
||||
};
|
||||
|
||||
mockRepository.findById.mockResolvedValueOnce(role);
|
||||
mockRepository.getPermissions.mockResolvedValueOnce(existingPermissions);
|
||||
mockPermissionDiffService.calculateDiff.mockReturnValueOnce(permissionChanges);
|
||||
mockRepository.update.mockResolvedValueOnce(role);
|
||||
|
||||
const result = await service.update({ roleId, permissions });
|
||||
expect(result).toBeDefined();
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(roleId, permissions, {
|
||||
|
||||
expect(mockPermissionDiffService.calculateDiff).toHaveBeenCalledWith(permissions, existingPermissions);
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(roleId, permissionChanges, {
|
||||
color: undefined,
|
||||
name: undefined,
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("should throw error if role does not exist", async () => {
|
||||
@@ -277,45 +310,18 @@ describe("RoleService", () => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockTrx = {
|
||||
selectFrom: vi.fn().mockReturnThis(),
|
||||
select: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
executeTakeFirst: vi.fn().mockResolvedValue({ id: roleId }),
|
||||
updateTable: vi.fn().mockReturnThis(),
|
||||
set: vi.fn().mockReturnThis(),
|
||||
execute: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
mockRepository.transaction.mockImplementationOnce((callback) => callback(mockRepository, mockTrx));
|
||||
mockRepository.findById.mockResolvedValueOnce(role);
|
||||
|
||||
const result = await service.assignRoleToMember(roleId, membershipId);
|
||||
expect(result).toEqual({ id: roleId });
|
||||
expect(mockTrx.selectFrom).toHaveBeenCalledWith("Role");
|
||||
expect(mockTrx.select).toHaveBeenCalledWith("id");
|
||||
expect(mockTrx.where).toHaveBeenCalledWith("id", "=", roleId);
|
||||
expect(mockTrx.executeTakeFirst).toHaveBeenCalled();
|
||||
expect(mockTrx.updateTable).toHaveBeenCalledWith("Membership");
|
||||
expect(mockTrx.set).toHaveBeenCalledWith({ customRoleId: roleId });
|
||||
expect(mockTrx.where).toHaveBeenCalledWith("id", "=", membershipId);
|
||||
expect(mockTrx.execute).toHaveBeenCalled();
|
||||
expect(result).toEqual(role); // Check the full role object
|
||||
});
|
||||
|
||||
it("should throw error if role does not exist", async () => {
|
||||
const mockTrx = {
|
||||
selectFrom: vi.fn().mockReturnThis(),
|
||||
select: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
executeTakeFirst: vi.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
mockRepository.transaction.mockImplementationOnce((callback) => callback(mockRepository, mockTrx));
|
||||
mockRepository.findById.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(service.assignRoleToMember(roleId, membershipId)).rejects.toThrow("Role not found");
|
||||
expect(mockTrx.selectFrom).toHaveBeenCalledWith("Role");
|
||||
expect(mockTrx.select).toHaveBeenCalledWith("id");
|
||||
expect(mockTrx.where).toHaveBeenCalledWith("id", "=", roleId);
|
||||
expect(mockTrx.executeTakeFirst).toHaveBeenCalled();
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith(roleId);
|
||||
// Do NOT expect transaction methods to be called, since the repository returns null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { RolePermission } from "../domain/models/Role";
|
||||
|
||||
export type PermissionString = string;
|
||||
export type ParsedPermission = { resource: string; action: string };
|
||||
|
||||
export class PermissionDiffService {
|
||||
private parsePermission(permission: PermissionString): ParsedPermission {
|
||||
const [resource, action] = permission.split(".");
|
||||
return { resource, action };
|
||||
}
|
||||
|
||||
private filterInternalPermissions(permissions: PermissionString[]): PermissionString[] {
|
||||
return permissions.filter((permission) => {
|
||||
const { action } = this.parsePermission(permission);
|
||||
return action !== "_resource";
|
||||
});
|
||||
}
|
||||
|
||||
private createPermissionKey(permission: ParsedPermission): string {
|
||||
return `${permission.resource}:${permission.action}`;
|
||||
}
|
||||
|
||||
calculateDiff(newPermissions: PermissionString[], existingPermissions: RolePermission[]) {
|
||||
const filteredPermissions = this.filterInternalPermissions(newPermissions);
|
||||
|
||||
// Create sets for comparison
|
||||
const existingSet = new Set(existingPermissions.map((p) => this.createPermissionKey(p)));
|
||||
|
||||
const newSet = new Set(
|
||||
filteredPermissions.map((p) => {
|
||||
const parsed = this.parsePermission(p);
|
||||
return this.createPermissionKey(parsed);
|
||||
})
|
||||
);
|
||||
|
||||
// Calculate permissions to add and remove
|
||||
const toAdd = filteredPermissions
|
||||
.map((p) => this.parsePermission(p))
|
||||
.filter((p) => !existingSet.has(this.createPermissionKey(p)));
|
||||
|
||||
const toRemove = existingPermissions.filter((p) => !newSet.has(this.createPermissionKey(p)));
|
||||
|
||||
return {
|
||||
toAdd,
|
||||
toRemove,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { RoleManagementError, RoleManagementErrorCode } from "../domain/errors/role-management.error";
|
||||
import { DEFAULT_ROLE_IDS } from "../lib/constants";
|
||||
import { PermissionCheckService } from "./permission-check.service";
|
||||
import { RoleService } from "./role.service";
|
||||
@@ -38,7 +37,10 @@ class PBACRoleManager implements IRoleManager {
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
throw new RoleManagementError(
|
||||
"You do not have permission to change roles",
|
||||
RoleManagementErrorCode.UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +57,10 @@ class PBACRoleManager implements IRoleManager {
|
||||
} else {
|
||||
const roleExists = await this.roleService.roleBelongsToTeam(role, organizationId);
|
||||
if (!roleExists) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "You do not have access to this role" });
|
||||
throw new RoleManagementError(
|
||||
"You do not have access to this role",
|
||||
RoleManagementErrorCode.INVALID_ROLE
|
||||
);
|
||||
}
|
||||
await this.roleService.assignRoleToMember(role, membershipId);
|
||||
}
|
||||
@@ -73,11 +78,14 @@ class PBACRoleManager implements IRoleManager {
|
||||
class LegacyRoleManager implements IRoleManager {
|
||||
public isPBACEnabled = false;
|
||||
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
|
||||
const isUpdaterAnOwner = await isOrganisationAdmin(userId, organizationId);
|
||||
const membership = await isOrganisationAdmin(userId, organizationId);
|
||||
|
||||
// Only OWNER can update role to OWNER
|
||||
if (!isUpdaterAnOwner) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
// Only OWNER/ADMIN can update role
|
||||
if (!membership) {
|
||||
throw new RoleManagementError(
|
||||
"Only owners or admin can update roles",
|
||||
RoleManagementErrorCode.UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kysely from "@calcom/kysely";
|
||||
import db from "@calcom/prisma";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { RoleType as DomainRoleType } from "../domain/models/Role";
|
||||
@@ -6,14 +6,14 @@ import type { CreateRoleData, UpdateRolePermissionsData } from "../domain/models
|
||||
import type { IRoleRepository } from "../domain/repositories/IRoleRepository";
|
||||
import { RoleRepository } from "../infrastructure/repositories/RoleRepository";
|
||||
import { DEFAULT_ROLE_IDS } from "../lib/constants";
|
||||
import { PermissionDiffService } from "./permission-diff.service";
|
||||
import { PermissionService } from "./permission.service";
|
||||
|
||||
// These IDs must match the ones in the migration
|
||||
|
||||
export class RoleService {
|
||||
constructor(
|
||||
private readonly repository: IRoleRepository = new RoleRepository(),
|
||||
private readonly permissionService: PermissionService = new PermissionService()
|
||||
private readonly permissionService: PermissionService = new PermissionService(),
|
||||
private readonly permissionDiffService: PermissionDiffService = new PermissionDiffService()
|
||||
) {}
|
||||
|
||||
async createRole(data: CreateRoleData) {
|
||||
@@ -37,19 +37,13 @@ export class RoleService {
|
||||
}
|
||||
|
||||
async assignRoleToMember(roleId: string, membershipId: number) {
|
||||
return this.repository.transaction(async (repo, trx) => {
|
||||
const role = await trx.selectFrom("Role").select("id").where("id", "=", roleId).executeTakeFirst();
|
||||
if (!role) throw new Error("Role not found");
|
||||
|
||||
// TODO: Move this to a MembershipRepository - bit difficult due to the trx record here.
|
||||
await trx
|
||||
.updateTable("Membership")
|
||||
.set({ customRoleId: roleId })
|
||||
.where("id", "=", membershipId)
|
||||
.execute();
|
||||
|
||||
return role;
|
||||
const role = await this.repository.findById(roleId);
|
||||
if (!role) throw new Error("Role not found");
|
||||
await db.membership.update({
|
||||
where: { id: membershipId },
|
||||
data: { customRoleId: roleId },
|
||||
});
|
||||
return role;
|
||||
}
|
||||
|
||||
async getRolePermissions(roleId: string) {
|
||||
@@ -58,12 +52,10 @@ export class RoleService {
|
||||
}
|
||||
|
||||
async removeRoleFromMember(membershipId: number) {
|
||||
// TODO: Move this to a MembershipRepository
|
||||
await kysely
|
||||
.updateTable("Membership")
|
||||
.set({ customRoleId: null })
|
||||
.where("id", "=", membershipId)
|
||||
.execute();
|
||||
await db.membership.update({
|
||||
where: { id: membershipId },
|
||||
data: { customRoleId: null },
|
||||
});
|
||||
}
|
||||
|
||||
async getRole(roleId: string) {
|
||||
@@ -79,12 +71,10 @@ export class RoleService {
|
||||
if (!role) {
|
||||
throw new Error("Role not found");
|
||||
}
|
||||
|
||||
// Don't allow deleting default roles
|
||||
if (role.type === DomainRoleType.SYSTEM) {
|
||||
throw new Error("Cannot delete default roles");
|
||||
}
|
||||
|
||||
await this.repository.delete(roleId);
|
||||
}
|
||||
|
||||
@@ -93,19 +83,20 @@ export class RoleService {
|
||||
if (!role) {
|
||||
throw new Error("Role not found");
|
||||
}
|
||||
|
||||
// Don't allow updating default roles
|
||||
if (role.type === DomainRoleType.SYSTEM) {
|
||||
throw new Error("Cannot update default roles");
|
||||
}
|
||||
|
||||
// Validate permissions
|
||||
const validationResult = this.permissionService.validatePermissions(data.permissions);
|
||||
if (!validationResult.isValid) {
|
||||
throw new Error(validationResult.error || "Invalid permissions provided");
|
||||
}
|
||||
|
||||
return this.repository.update(data.roleId, data.permissions, {
|
||||
const existingPermissions = await this.repository.getPermissions(data.roleId);
|
||||
const permissionChanges = this.permissionDiffService.calculateDiff(data.permissions, existingPermissions);
|
||||
|
||||
return this.repository.update(data.roleId, permissionChanges, {
|
||||
color: data.updates?.color,
|
||||
name: data.updates?.name,
|
||||
});
|
||||
|
||||
@@ -556,60 +556,55 @@ const CopyTimes = ({
|
||||
<p className="h6 text-emphasis pb-3 pl-1 text-xs font-medium uppercase">{t("copy_times_to")}</p>
|
||||
<ol className="space-y-2">
|
||||
<li key="select all">
|
||||
<label className="text-default flex w-full items-center justify-between">
|
||||
<span className="px-1">{t("select_all")}</span>
|
||||
<CheckboxField
|
||||
description=""
|
||||
value={t("select_all")}
|
||||
checked={selected.length === 7}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelected([0, 1, 2, 3, 4, 5, 6]);
|
||||
} else if (!e.target.checked) {
|
||||
setSelected([]);
|
||||
}
|
||||
}}
|
||||
ref={(ref) => {
|
||||
if (ref) {
|
||||
itteratablesByKeyRef.current.push(ref as HTMLInputElement);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<CheckboxField
|
||||
description={t("select_all")}
|
||||
descriptionAsLabel
|
||||
value={t("select_all")}
|
||||
checked={selected.length === 7}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelected([0, 1, 2, 3, 4, 5, 6]);
|
||||
} else if (!e.target.checked) {
|
||||
setSelected([]);
|
||||
}
|
||||
}}
|
||||
ref={(ref) => {
|
||||
if (ref) {
|
||||
itteratablesByKeyRef.current.push(ref as HTMLInputElement);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
{weekdayNames(i18n.language, weekStart).map((weekday, num) => {
|
||||
const weekdayIndex = (num + weekStart) % 7;
|
||||
return (
|
||||
<li key={weekday}>
|
||||
<label className="text-default flex w-full items-center justify-between">
|
||||
<span className="px-1">{weekday}</span>
|
||||
<CheckboxField
|
||||
description=""
|
||||
value={weekdayIndex}
|
||||
checked={selected.includes(weekdayIndex) || disabled === weekdayIndex}
|
||||
disabled={disabled === weekdayIndex}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked && !selected.includes(weekdayIndex)) {
|
||||
setSelected(selected.concat([weekdayIndex]));
|
||||
} else if (!e.target.checked && selected.includes(weekdayIndex)) {
|
||||
setSelected(selected.filter((item) => item !== weekdayIndex));
|
||||
}
|
||||
}}
|
||||
ref={(ref) => {
|
||||
if (ref && disabled !== weekdayIndex) {
|
||||
//we don't need to iterate over disabled elements
|
||||
itteratablesByKeyRef.current.push(ref as HTMLInputElement);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<CheckboxField
|
||||
description={weekday}
|
||||
descriptionAsLabel
|
||||
value={weekdayIndex}
|
||||
checked={selected.includes(weekdayIndex) || disabled === weekdayIndex}
|
||||
disabled={disabled === weekdayIndex}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked && !selected.includes(weekdayIndex)) {
|
||||
setSelected(selected.concat([weekdayIndex]));
|
||||
} else if (!e.target.checked && selected.includes(weekdayIndex)) {
|
||||
setSelected(selected.filter((item) => item !== weekdayIndex));
|
||||
}
|
||||
}}
|
||||
ref={(ref) => {
|
||||
if (ref && disabled !== weekdayIndex) {
|
||||
itteratablesByKeyRef.current.push(ref as HTMLInputElement);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
<hr className="border-subtle" />
|
||||
<div className="space-x-2 px-2 rtl:space-x-reverse">
|
||||
<div className="flex justify-end space-x-2 px-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
color="minimal"
|
||||
onClick={() => onCancel()}
|
||||
|
||||
@@ -21,7 +21,8 @@ export const createAProfileForAnExistingUser = async ({
|
||||
};
|
||||
organizationId: number;
|
||||
}) => {
|
||||
const org = await TeamRepository.findById({ id: organizationId });
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
const org = await teamRepo.findById({ id: organizationId });
|
||||
if (!org) {
|
||||
throw new Error(`Organization with id ${organizationId} not found`);
|
||||
}
|
||||
@@ -47,7 +48,8 @@ export const createAProfileForAnExistingUser = async ({
|
||||
movedFromUserId: user.id,
|
||||
});
|
||||
|
||||
await UserRepository.updateWhereId({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
await userRepo.updateWhereId({
|
||||
whereId: user.id,
|
||||
data: {
|
||||
movedToProfileId: profile.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
import type { ServiceAccountKey } from "@calcom/lib/server/repository/delegationCredential";
|
||||
import { DelegationCredentialRepository } from "@calcom/lib/server/repository/delegationCredential";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import { UserRepository } from "../server/repository/user";
|
||||
@@ -593,7 +594,7 @@ export async function findUniqueDelegationCalendarCredential({
|
||||
}) {
|
||||
const [delegationCredential, user] = await Promise.all([
|
||||
DelegationCredentialRepository.findByIdIncludeSensitiveServiceAccountKey({ id: delegationCredentialId }),
|
||||
UserRepository.findById({ id: userId }),
|
||||
new UserRepository(prisma).findById({ id: userId }),
|
||||
]);
|
||||
|
||||
if (!delegationCredential) {
|
||||
|
||||
@@ -66,11 +66,12 @@ export const getEventTypeById = async ({
|
||||
const newMetadata = eventTypeMetaDataSchemaWithTypedApps.parse(metadata || {}) || {};
|
||||
const apps = newMetadata?.apps || {};
|
||||
const eventTypeWithParsedMetadata = { ...rawEventType, metadata: newMetadata };
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const eventTeamMembershipsWithUserProfile = [];
|
||||
for (const eventTeamMembership of rawEventType.team?.members || []) {
|
||||
eventTeamMembershipsWithUserProfile.push({
|
||||
...eventTeamMembership,
|
||||
user: await UserRepository.enrichUserWithItsProfile({
|
||||
user: await userRepo.enrichUserWithItsProfile({
|
||||
user: eventTeamMembership.user,
|
||||
}),
|
||||
});
|
||||
@@ -81,7 +82,7 @@ export const getEventTypeById = async ({
|
||||
childrenWithUserProfile.push({
|
||||
...child,
|
||||
owner: child.owner
|
||||
? await UserRepository.enrichUserWithItsProfile({
|
||||
? await userRepo.enrichUserWithItsProfile({
|
||||
user: child.owner,
|
||||
})
|
||||
: null,
|
||||
@@ -91,7 +92,7 @@ export const getEventTypeById = async ({
|
||||
const eventTypeUsersWithUserProfile = [];
|
||||
for (const eventTypeUser of rawEventType.users) {
|
||||
eventTypeUsersWithUserProfile.push(
|
||||
await UserRepository.enrichUserWithItsProfile({
|
||||
await userRepo.enrichUserWithItsProfile({
|
||||
user: eventTypeUser,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeMetaDataSchemaWithUntypedApps } from "@calcom/prisma/zod-utils";
|
||||
@@ -105,32 +106,35 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo
|
||||
|
||||
type UserEventTypes = (typeof profileEventTypes)[number];
|
||||
|
||||
const mapEventType = async (eventType: UserEventTypes) => ({
|
||||
...eventType,
|
||||
safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined,
|
||||
users: await Promise.all(
|
||||
(!!eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users).map(
|
||||
async (u) =>
|
||||
await UserRepository.enrichUserWithItsProfile({
|
||||
user: u,
|
||||
})
|
||||
)
|
||||
),
|
||||
metadata: eventType.metadata ? eventTypeMetaDataSchemaWithUntypedApps.parse(eventType.metadata) : null,
|
||||
children: await Promise.all(
|
||||
(eventType.children || []).map(async (c) => ({
|
||||
...c,
|
||||
users: await Promise.all(
|
||||
c.users.map(
|
||||
async (u) =>
|
||||
await UserRepository.enrichUserWithItsProfile({
|
||||
user: u,
|
||||
})
|
||||
)
|
||||
),
|
||||
}))
|
||||
),
|
||||
});
|
||||
const mapEventType = async (eventType: UserEventTypes) => {
|
||||
const userRepo = new UserRepository(prisma);
|
||||
return {
|
||||
...eventType,
|
||||
safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined,
|
||||
users: await Promise.all(
|
||||
(!!eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users).map(
|
||||
async (u) =>
|
||||
await userRepo.enrichUserWithItsProfile({
|
||||
user: u,
|
||||
})
|
||||
)
|
||||
),
|
||||
metadata: eventType.metadata ? eventTypeMetaDataSchemaWithUntypedApps.parse(eventType.metadata) : null,
|
||||
children: await Promise.all(
|
||||
(eventType.children || []).map(async (c) => ({
|
||||
...c,
|
||||
users: await Promise.all(
|
||||
c.users.map(
|
||||
async (u) =>
|
||||
await userRepo.enrichUserWithItsProfile({
|
||||
user: u,
|
||||
})
|
||||
)
|
||||
),
|
||||
}))
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const userEventTypes = (await Promise.all(profileEventTypes.map(mapEventType))).filter((eventType) => {
|
||||
const isAChildEvent = eventType.parentId;
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
|
||||
import { getDefinedBufferTimes } from "../features/eventtypes/lib/getDefinedBufferTimes";
|
||||
import { BookingRepository as BookingRepo } from "./server/repository/booking";
|
||||
import { BookingRepository } from "./server/repository/booking";
|
||||
|
||||
const _getBusyTimes = async (params: {
|
||||
credentials: CredentialForCalendarService[];
|
||||
@@ -108,7 +108,8 @@ const _getBusyTimes = async (params: {
|
||||
let bookings = params.currentBookings;
|
||||
|
||||
if (!bookings) {
|
||||
bookings = await BookingRepo.findAllExistingBookingsForEventTypeBetween({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
|
||||
userIdAndEmailMap: new Map([[userId, userEmail]]),
|
||||
eventTypeId,
|
||||
startDate: startTimeAdjustedWithMaxBuffer,
|
||||
|
||||
@@ -699,6 +699,12 @@ type GetUsersAvailabilityProps = {
|
||||
};
|
||||
|
||||
const _getUsersAvailability = async ({ users, query, initialData }: GetUsersAvailabilityProps) => {
|
||||
if (users.length >= 50) {
|
||||
const userIds = users.map(({ id }) => id).join(", ");
|
||||
log.warn(
|
||||
`High-load warning: Attempting to fetch availability for ${users.length} users. User IDs: [${userIds}], EventTypeId: [${query.eventTypeId}]`
|
||||
);
|
||||
}
|
||||
return await Promise.all(
|
||||
users.map((user) =>
|
||||
_getUserAvailability(
|
||||
|
||||
@@ -77,7 +77,8 @@ async function _checkBookingLimit({
|
||||
let bookingsInPeriod;
|
||||
|
||||
if (teamId && user) {
|
||||
bookingsInPeriod = await BookingRepository.getAllAcceptedTeamBookingsOfUser({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
bookingsInPeriod = await bookingRepo.getAllAcceptedTeamBookingsOfUser({
|
||||
user: { id: user.id, email: user.email },
|
||||
teamId,
|
||||
startDate: startDate,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import { getTotalBookingDuration } from "@calcom/lib/server/queries/booking";
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
|
||||
import { descendingLimitKeys, intervalLimitKeyToUnit } from "../intervalLimit";
|
||||
@@ -233,7 +234,8 @@ const _getBusyTimesFromTeamLimits = async (
|
||||
bookingLimits
|
||||
);
|
||||
|
||||
const bookings = await BookingRepository.getAllAcceptedTeamBookingsOfUser({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const bookings = await bookingRepo.getAllAcceptedTeamBookingsOfUser({
|
||||
user,
|
||||
teamId,
|
||||
startDate: limitDateFrom.toDate(),
|
||||
|
||||
@@ -490,7 +490,8 @@ async function getBookingsOfInterval({
|
||||
rrTimestampBasis: RRTimestampBasis;
|
||||
meetingStartTime?: Date;
|
||||
}) {
|
||||
return await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
return await bookingRepo.getAllBookingsForRoundRobin({
|
||||
eventTypeId: eventTypeId,
|
||||
users,
|
||||
startDate: getIntervalStartDate({ interval, rrTimestampBasis, meetingStartTime }),
|
||||
|
||||
@@ -23,7 +23,13 @@ import { getRoutedUrl } from "./getRoutedUrl";
|
||||
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
|
||||
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
|
||||
vi.mock("@calcom/lib/server/repository/routingForm");
|
||||
vi.mock("@calcom/lib/server/repository/user");
|
||||
vi.mock("@calcom/lib/server/repository/user", () => {
|
||||
return {
|
||||
UserRepository: vi.fn().mockImplementation(() => ({
|
||||
enrichUserWithItsProfile: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
vi.mock("@calcom/features/ee/organizations/lib/orgDomains");
|
||||
vi.mock("@calcom/features/routing-forms/lib/isAuthorizedToViewForm");
|
||||
vi.mock("@calcom/app-store/routing-forms/lib/getSerializableForm");
|
||||
@@ -79,7 +85,17 @@ describe("getRoutedUrl", () => {
|
||||
// Provide default mock implementations
|
||||
vi.mocked(orgDomainConfig).mockReturnValue({ currentOrgDomain: null });
|
||||
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
|
||||
vi.mocked(UserRepository.enrichUserWithItsProfile).mockImplementation(async ({ user }) => user);
|
||||
|
||||
const mockEnrichUserWithItsProfile = vi.fn().mockImplementation(async ({ user }) => user);
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
vi.mocked(isAuthorizedToViewFormOnOrgDomain).mockReturnValue(true);
|
||||
vi.mocked(getSerializableForm).mockResolvedValue(mockSerializableForm as never);
|
||||
vi.mocked(findMatchingRoute).mockReturnValue(null);
|
||||
|
||||
@@ -22,6 +22,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -102,9 +103,10 @@ const _getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "query" |
|
||||
}
|
||||
|
||||
const profileEnrichmentStart = performance.now();
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const formWithUserProfile = {
|
||||
...form,
|
||||
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
|
||||
user: await userRepo.enrichUserWithItsProfile({ user: form.user }),
|
||||
};
|
||||
timeTaken.profileEnrichment = performance.now() - profileEnrichmentStart;
|
||||
|
||||
|
||||
@@ -183,10 +183,11 @@ export async function getTeamWithMembers(args: {
|
||||
if (!teamOrOrg) return null;
|
||||
|
||||
const teamOrOrgMemberships = [];
|
||||
const userRepo = new UserRepository(prisma);
|
||||
for (const membership of teamOrOrg.members) {
|
||||
teamOrOrgMemberships.push({
|
||||
...membership,
|
||||
user: await UserRepository.enrichUserWithItsProfile({
|
||||
user: await userRepo.enrichUserWithItsProfile({
|
||||
user: membership.user,
|
||||
}),
|
||||
});
|
||||
@@ -236,7 +237,7 @@ export async function getTeamWithMembers(args: {
|
||||
const usersWithUserProfile = [];
|
||||
for (const { user } of eventType.hosts) {
|
||||
usersWithUserProfile.push(
|
||||
await UserRepository.enrichUserWithItsProfile({
|
||||
await userRepo.enrichUserWithItsProfile({
|
||||
user,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -6,8 +6,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { TeamRepository } from "./team";
|
||||
|
||||
describe("TeamRepository", () => {
|
||||
let teamRepository: TeamRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
teamRepository = new TeamRepository(prismaMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -17,7 +20,7 @@ describe("TeamRepository", () => {
|
||||
describe("findById", () => {
|
||||
it("should return null if team is not found", async () => {
|
||||
prismaMock.team.findUnique.mockResolvedValue(null);
|
||||
const result = await TeamRepository.findById({ id: 1 });
|
||||
const result = await teamRepository.findById({ id: 1 });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -37,7 +40,7 @@ describe("TeamRepository", () => {
|
||||
requestedSlug: null,
|
||||
};
|
||||
prismaMock.team.findUnique.mockResolvedValue(mockTeam as unknown as Team);
|
||||
const result = await TeamRepository.findById({ id: 1 });
|
||||
const result = await teamRepository.findById({ id: 1 });
|
||||
expect(result).toEqual(mockTeam);
|
||||
});
|
||||
});
|
||||
@@ -67,7 +70,7 @@ describe("TeamRepository", () => {
|
||||
return callback(mockTx);
|
||||
});
|
||||
|
||||
const result = await TeamRepository.deleteById({ id: 1 });
|
||||
const result = await teamRepository.deleteById({ id: 1 });
|
||||
|
||||
expect(deleteManyEventTypeMock).toHaveBeenCalledWith({
|
||||
where: {
|
||||
@@ -93,7 +96,7 @@ describe("TeamRepository", () => {
|
||||
it("should return all teams with given parentId", async () => {
|
||||
const mockTeams = [{ id: 1 }, { id: 2 }];
|
||||
prismaMock.team.findMany.mockResolvedValue(mockTeams as unknown as Team[]);
|
||||
const result = await TeamRepository.findAllByParentId({ parentId: 1 });
|
||||
const result = await teamRepository.findAllByParentId({ parentId: 1 });
|
||||
expect(prismaMock.team.findMany).toHaveBeenCalledWith({
|
||||
where: { parentId: 1 },
|
||||
select: {
|
||||
@@ -116,7 +119,7 @@ describe("TeamRepository", () => {
|
||||
it("should return team with its members", async () => {
|
||||
const mockTeam = { id: 1, members: [] };
|
||||
prismaMock.team.findUnique.mockResolvedValue(mockTeam as unknown as Team & { members: [] });
|
||||
const result = await TeamRepository.findTeamWithMembers(1);
|
||||
const result = await teamRepository.findTeamWithMembers(1);
|
||||
expect(prismaMock.team.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
select: {
|
||||
|
||||
@@ -104,7 +104,8 @@ describe("BookingRepository", () => {
|
||||
it("it should not include bookings where the attendee is a no show", async () => {
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const bookings = await bookingRepo.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
@@ -118,7 +119,8 @@ describe("BookingRepository", () => {
|
||||
it("it should include bookings where the attendee is a no show", async () => {
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const bookings = await bookingRepo.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
@@ -132,7 +134,8 @@ describe("BookingRepository", () => {
|
||||
it("it should not include bookings where the host is a no show", async () => {
|
||||
await createHostNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const bookings = await bookingRepo.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
@@ -146,7 +149,8 @@ describe("BookingRepository", () => {
|
||||
it("it should include bookings where the host is a no show", async () => {
|
||||
await createHostNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const bookings = await bookingRepo.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
@@ -161,7 +165,8 @@ describe("BookingRepository", () => {
|
||||
await createHostNoShowTestBookings();
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const bookings = await bookingRepo.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
@@ -176,7 +181,8 @@ describe("BookingRepository", () => {
|
||||
await createHostNoShowTestBookings();
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const bookings = await bookingRepo.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
@@ -232,7 +238,8 @@ describe("BookingRepository", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
const bookingRepo = new BookingRepository(prismaMock);
|
||||
const bookings = await bookingRepo.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date("2025-06-01T00:00:00.000Z"),
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { Prisma } from "@prisma/client";
|
||||
|
||||
import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma/client";
|
||||
import { RRTimestampBasis } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
@@ -113,8 +114,10 @@ const buildWhereClauseForActiveBookings = ({
|
||||
});
|
||||
|
||||
export class BookingRepository {
|
||||
static async getBookingAttendees(bookingId: number) {
|
||||
return await prisma.attendee.findMany({
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async getBookingAttendees(bookingId: number) {
|
||||
return await this.prismaClient.attendee.findMany({
|
||||
where: {
|
||||
bookingId,
|
||||
},
|
||||
@@ -122,8 +125,8 @@ export class BookingRepository {
|
||||
}
|
||||
|
||||
/** Determines if the user is the organizer, team admin, or org admin that the booking was created under */
|
||||
static async doesUserIdHaveAccessToBooking({ userId, bookingId }: { userId: number; bookingId: number }) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
async doesUserIdHaveAccessToBooking({ userId, bookingId }: { userId: number; bookingId: number }) {
|
||||
const booking = await this.prismaClient.booking.findUnique({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
@@ -145,7 +148,8 @@ export class BookingRepository {
|
||||
if (!booking.eventType || !booking.eventType.teamId) return false;
|
||||
|
||||
// TODO add checks for team and org
|
||||
const isAdminOrUser = await UserRepository.isAdminOfTeamOrParentOrg({
|
||||
const userRepo = new UserRepository(this.prismaClient);
|
||||
const isAdminOrUser = await userRepo.isAdminOfTeamOrParentOrg({
|
||||
userId,
|
||||
teamId: booking.eventType.teamId,
|
||||
});
|
||||
@@ -153,8 +157,8 @@ export class BookingRepository {
|
||||
return isAdminOrUser;
|
||||
}
|
||||
|
||||
static async findFirstBookingByReschedule({ originalBookingUid }: { originalBookingUid: string }) {
|
||||
return await prisma.booking.findFirst({
|
||||
async findFirstBookingByReschedule({ originalBookingUid }: { originalBookingUid: string }) {
|
||||
return await this.prismaClient.booking.findFirst({
|
||||
where: {
|
||||
fromReschedule: originalBookingUid,
|
||||
},
|
||||
@@ -164,8 +168,8 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findReschedulerByUid({ uid }: { uid: string }) {
|
||||
return await prisma.booking.findUnique({
|
||||
async findReschedulerByUid({ uid }: { uid: string }) {
|
||||
return await this.prismaClient.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
@@ -176,7 +180,7 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
private static async _findAllExistingBookingsForEventTypeBetween({
|
||||
private async _findAllExistingBookingsForEventTypeBetween({
|
||||
eventTypeId,
|
||||
seatedEvent = false,
|
||||
startDate,
|
||||
@@ -227,7 +231,7 @@ export class BookingRepository {
|
||||
}),
|
||||
} satisfies Prisma.BookingSelect;
|
||||
|
||||
const currentBookingsAllUsersQueryOne = prisma.booking.findMany({
|
||||
const currentBookingsAllUsersQueryOne = this.prismaClient.booking.findMany({
|
||||
where: {
|
||||
...sharedQuery,
|
||||
userId: {
|
||||
@@ -237,7 +241,7 @@ export class BookingRepository {
|
||||
select: bookingsSelect,
|
||||
});
|
||||
|
||||
const currentBookingsAllUsersQueryTwo = prisma.booking.findMany({
|
||||
const currentBookingsAllUsersQueryTwo = this.prismaClient.booking.findMany({
|
||||
where: {
|
||||
...sharedQuery,
|
||||
attendees: {
|
||||
@@ -252,7 +256,7 @@ export class BookingRepository {
|
||||
});
|
||||
|
||||
const currentBookingsAllUsersQueryThree = eventTypeId
|
||||
? prisma.booking.findMany({
|
||||
? this.prismaClient.booking.findMany({
|
||||
where: {
|
||||
startTime: { lte: endDate },
|
||||
endTime: { gte: startDate },
|
||||
@@ -288,12 +292,12 @@ export class BookingRepository {
|
||||
return [...resultOne, ...resultTwoWithOrganizersRemoved, ...resultThree];
|
||||
}
|
||||
|
||||
static findAllExistingBookingsForEventTypeBetween = withReporting(
|
||||
BookingRepository._findAllExistingBookingsForEventTypeBetween,
|
||||
findAllExistingBookingsForEventTypeBetween = withReporting(
|
||||
this._findAllExistingBookingsForEventTypeBetween.bind(this),
|
||||
"findAllExistingBookingsForEventTypeBetween"
|
||||
);
|
||||
|
||||
static async getAllBookingsForRoundRobin({
|
||||
async getAllBookingsForRoundRobin({
|
||||
users,
|
||||
eventTypeId,
|
||||
startDate,
|
||||
@@ -316,7 +320,7 @@ export class BookingRepository {
|
||||
includeNoShowInRRCalculation: boolean;
|
||||
rrTimestampBasis: RRTimestampBasis;
|
||||
}) {
|
||||
const allBookings = await prisma.booking.findMany({
|
||||
const allBookings = await this.prismaClient.booking.findMany({
|
||||
where: buildWhereClauseForActiveBookings({
|
||||
eventTypeId,
|
||||
startDate,
|
||||
@@ -366,8 +370,8 @@ export class BookingRepository {
|
||||
return queueBookings;
|
||||
}
|
||||
|
||||
static async findBookingByUid({ bookingUid }: { bookingUid: string }) {
|
||||
return await prisma.booking.findUnique({
|
||||
async findBookingByUid({ bookingUid }: { bookingUid: string }) {
|
||||
return await this.prismaClient.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
@@ -375,8 +379,8 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findBookingForMeetingPage({ bookingUid }: { bookingUid: string }) {
|
||||
return await prisma.booking.findUnique({
|
||||
async findBookingForMeetingPage({ bookingUid }: { bookingUid: string }) {
|
||||
return await this.prismaClient.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
@@ -430,8 +434,8 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findBookingForMeetingEndedPage({ bookingUid }: { bookingUid: string }) {
|
||||
return await prisma.booking.findUnique({
|
||||
async findBookingForMeetingEndedPage({ bookingUid }: { bookingUid: string }) {
|
||||
return await this.prismaClient.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
@@ -454,8 +458,8 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findBookingByUidAndUserId({ bookingUid, userId }: { bookingUid: string; userId: number }) {
|
||||
return await prisma.booking.findFirst({
|
||||
async findBookingByUidAndUserId({ bookingUid, userId }: { bookingUid: string; userId: number }) {
|
||||
return await this.prismaClient.booking.findFirst({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
OR: [
|
||||
@@ -515,7 +519,7 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async updateLocationById({
|
||||
async updateLocationById({
|
||||
where: { id },
|
||||
data: { location, metadata, referencesToCreate },
|
||||
}: {
|
||||
@@ -526,7 +530,7 @@ export class BookingRepository {
|
||||
referencesToCreate: Prisma.BookingReferenceCreateInput[];
|
||||
};
|
||||
}) {
|
||||
await prisma.booking.update({
|
||||
await this.prismaClient.booking.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -540,13 +544,11 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsWithCount): Promise<number>;
|
||||
async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsWithCount): Promise<number>;
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUser(
|
||||
params: TeamBookingsParamsWithoutCount
|
||||
): Promise<Array<Booking>>;
|
||||
async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsWithoutCount): Promise<Array<Booking>>;
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsBase) {
|
||||
async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsBase) {
|
||||
const { user, teamId, startDate, endDate, excludedUid, shouldReturnCount, includeManagedEvents } = params;
|
||||
|
||||
const baseWhere: Prisma.BookingWhereInput = {
|
||||
@@ -595,18 +597,18 @@ export class BookingRepository {
|
||||
};
|
||||
|
||||
if (shouldReturnCount) {
|
||||
const collectiveRoundRobinBookingsOwner = await prisma.booking.count({
|
||||
const collectiveRoundRobinBookingsOwner = await this.prismaClient.booking.count({
|
||||
where: whereCollectiveRoundRobinOwner,
|
||||
});
|
||||
|
||||
const collectiveRoundRobinBookingsAttendee = await prisma.booking.count({
|
||||
const collectiveRoundRobinBookingsAttendee = await this.prismaClient.booking.count({
|
||||
where: whereCollectiveRoundRobinBookingsAttendee,
|
||||
});
|
||||
|
||||
let managedBookings = 0;
|
||||
|
||||
if (includeManagedEvents) {
|
||||
managedBookings = await prisma.booking.count({
|
||||
managedBookings = await this.prismaClient.booking.count({
|
||||
where: whereManagedBookings,
|
||||
});
|
||||
}
|
||||
@@ -616,18 +618,18 @@ export class BookingRepository {
|
||||
|
||||
return totalNrOfBooking;
|
||||
}
|
||||
const collectiveRoundRobinBookingsOwner = await prisma.booking.findMany({
|
||||
const collectiveRoundRobinBookingsOwner = await this.prismaClient.booking.findMany({
|
||||
where: whereCollectiveRoundRobinOwner,
|
||||
});
|
||||
|
||||
const collectiveRoundRobinBookingsAttendee = await prisma.booking.findMany({
|
||||
const collectiveRoundRobinBookingsAttendee = await this.prismaClient.booking.findMany({
|
||||
where: whereCollectiveRoundRobinBookingsAttendee,
|
||||
});
|
||||
|
||||
let managedBookings: typeof collectiveRoundRobinBookingsAttendee = [];
|
||||
|
||||
if (includeManagedEvents) {
|
||||
managedBookings = await prisma.booking.findMany({
|
||||
managedBookings = await this.prismaClient.booking.findMany({
|
||||
where: whereManagedBookings,
|
||||
});
|
||||
}
|
||||
@@ -639,8 +641,8 @@ export class BookingRepository {
|
||||
];
|
||||
}
|
||||
|
||||
static async findOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
||||
return await prisma.booking.findFirst({
|
||||
async findOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
||||
return await this.prismaClient.booking.findFirst({
|
||||
where: {
|
||||
uid: uid,
|
||||
status: {
|
||||
@@ -692,15 +694,13 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUsers(
|
||||
params: TeamBookingsMultipleUsersParamsWithCount
|
||||
): Promise<number>;
|
||||
async getAllAcceptedTeamBookingsOfUsers(params: TeamBookingsMultipleUsersParamsWithCount): Promise<number>;
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUsers(
|
||||
async getAllAcceptedTeamBookingsOfUsers(
|
||||
params: TeamBookingsMultipleUsersParamsWithoutCount
|
||||
): Promise<Array<Booking>>;
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUsers(params: TeamBookingsMultipleUsersParamsBase) {
|
||||
async getAllAcceptedTeamBookingsOfUsers(params: TeamBookingsMultipleUsersParamsBase) {
|
||||
const { users, teamId, startDate, endDate, excludedUid, shouldReturnCount, includeManagedEvents } =
|
||||
params;
|
||||
|
||||
@@ -759,18 +759,18 @@ export class BookingRepository {
|
||||
};
|
||||
|
||||
if (shouldReturnCount) {
|
||||
const collectiveRoundRobinBookingsOwner = await prisma.booking.count({
|
||||
const collectiveRoundRobinBookingsOwner = await this.prismaClient.booking.count({
|
||||
where: whereCollectiveRoundRobinOwner,
|
||||
});
|
||||
|
||||
const collectiveRoundRobinBookingsAttendee = await prisma.booking.count({
|
||||
const collectiveRoundRobinBookingsAttendee = await this.prismaClient.booking.count({
|
||||
where: whereCollectiveRoundRobinBookingsAttendee,
|
||||
});
|
||||
|
||||
let managedBookings = 0;
|
||||
|
||||
if (includeManagedEvents) {
|
||||
managedBookings = await prisma.booking.count({
|
||||
managedBookings = await this.prismaClient.booking.count({
|
||||
where: whereManagedBookings,
|
||||
});
|
||||
}
|
||||
@@ -781,18 +781,18 @@ export class BookingRepository {
|
||||
return totalNrOfBooking;
|
||||
}
|
||||
|
||||
const collectiveRoundRobinBookingsOwner = await prisma.booking.findMany({
|
||||
const collectiveRoundRobinBookingsOwner = await this.prismaClient.booking.findMany({
|
||||
where: whereCollectiveRoundRobinOwner,
|
||||
});
|
||||
|
||||
const collectiveRoundRobinBookingsAttendee = await prisma.booking.findMany({
|
||||
const collectiveRoundRobinBookingsAttendee = await this.prismaClient.booking.findMany({
|
||||
where: whereCollectiveRoundRobinBookingsAttendee,
|
||||
});
|
||||
|
||||
let managedBookings: typeof collectiveRoundRobinBookingsAttendee = [];
|
||||
|
||||
if (includeManagedEvents) {
|
||||
managedBookings = await prisma.booking.findMany({
|
||||
managedBookings = await this.prismaClient.booking.findMany({
|
||||
where: whereManagedBookings,
|
||||
});
|
||||
}
|
||||
@@ -804,7 +804,7 @@ export class BookingRepository {
|
||||
];
|
||||
}
|
||||
|
||||
static async getValidBookingFromEventTypeForAttendee({
|
||||
async getValidBookingFromEventTypeForAttendee({
|
||||
eventTypeId,
|
||||
bookerEmail,
|
||||
bookerPhoneNumber,
|
||||
@@ -817,7 +817,7 @@ export class BookingRepository {
|
||||
startTime: Date;
|
||||
filterForUnconfirmed?: boolean;
|
||||
}) {
|
||||
return await prisma.booking.findFirst({
|
||||
return await this.prismaClient.booking.findFirst({
|
||||
where: {
|
||||
eventTypeId,
|
||||
attendees: {
|
||||
|
||||
@@ -93,7 +93,8 @@ export class OrganizationRepository {
|
||||
logger.debug("createWithNonExistentOwner", safeStringify({ orgData, owner }));
|
||||
const organization = await this.create(orgData);
|
||||
const ownerUsernameInOrg = getOrgUsernameFromEmail(owner.email, orgData.autoAcceptEmail);
|
||||
const ownerInDb = await UserRepository.create({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const ownerInDb = await userRepo.create({
|
||||
email: owner.email,
|
||||
username: ownerUsernameInOrg,
|
||||
organizationId: organization.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type WhereCondition = {
|
||||
slotUtcStartDate?: Date | string;
|
||||
@@ -23,17 +23,19 @@ export type TimeSlot = {
|
||||
};
|
||||
|
||||
export class SelectedSlotsRepository {
|
||||
static async findMany({ where, select }: FindManyArgs) {
|
||||
return await prisma.selectedSlots.findMany({ where, select });
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async findMany({ where, select }: FindManyArgs) {
|
||||
return await this.prismaClient.selectedSlots.findMany({ where, select });
|
||||
}
|
||||
|
||||
static async findFirst({ where }: { where: Prisma.SelectedSlotsWhereInput }) {
|
||||
return await prisma.selectedSlots.findFirst({
|
||||
async findFirst({ where }: { where: Prisma.SelectedSlotsWhereInput }) {
|
||||
return await this.prismaClient.selectedSlots.findFirst({
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
static async findReservedByOthers({
|
||||
async findReservedByOthers({
|
||||
slot,
|
||||
eventTypeId,
|
||||
uid,
|
||||
@@ -53,7 +55,7 @@ export class SelectedSlotsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findManyReservedByOthers(slots: TimeSlot[], eventTypeId: number, uid: string) {
|
||||
async findManyReservedByOthers(slots: TimeSlot[], eventTypeId: number, uid: string) {
|
||||
return await this.findMany({
|
||||
where: {
|
||||
OR: slots.map((slot) => ({
|
||||
@@ -71,14 +73,14 @@ export class SelectedSlotsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findManyUnexpiredSlots({
|
||||
async findManyUnexpiredSlots({
|
||||
userIds,
|
||||
currentTimeInUtc,
|
||||
}: {
|
||||
userIds: number[];
|
||||
currentTimeInUtc: string;
|
||||
}) {
|
||||
return prisma.selectedSlots.findMany({
|
||||
return this.prismaClient.selectedSlots.findMany({
|
||||
where: {
|
||||
userId: { in: userIds },
|
||||
releaseAt: { gt: currentTimeInUtc },
|
||||
@@ -95,14 +97,14 @@ export class SelectedSlotsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async deleteManyExpiredSlots({
|
||||
async deleteManyExpiredSlots({
|
||||
eventTypeId,
|
||||
currentTimeInUtc,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
currentTimeInUtc: string;
|
||||
}) {
|
||||
return prisma.selectedSlots.deleteMany({
|
||||
return this.prismaClient.selectedSlots.deleteMany({
|
||||
where: {
|
||||
eventTypeId: { equals: eventTypeId },
|
||||
releaseAt: { lt: currentTimeInUtc },
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { z } from "zod";
|
||||
|
||||
import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -166,8 +167,10 @@ const teamSelect = {
|
||||
} satisfies Prisma.TeamSelect;
|
||||
|
||||
export class TeamRepository {
|
||||
static async findById({ id }: { id: number }) {
|
||||
const team = await prisma.team.findUnique({
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async findById({ id }: { id: number }) {
|
||||
const team = await this.prismaClient.team.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -179,14 +182,14 @@ export class TeamRepository {
|
||||
return getParsedTeam(team);
|
||||
}
|
||||
|
||||
static async findAllByParentId({
|
||||
async findAllByParentId({
|
||||
parentId,
|
||||
select = teamSelect,
|
||||
}: {
|
||||
parentId: number;
|
||||
select?: Prisma.TeamSelect;
|
||||
}) {
|
||||
return await prisma.team.findMany({
|
||||
return await this.prismaClient.team.findMany({
|
||||
where: {
|
||||
parentId,
|
||||
},
|
||||
@@ -194,7 +197,7 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findByIdAndParentId({
|
||||
async findByIdAndParentId({
|
||||
id,
|
||||
parentId,
|
||||
select = teamSelect,
|
||||
@@ -203,7 +206,7 @@ export class TeamRepository {
|
||||
parentId: number;
|
||||
select?: Prisma.TeamSelect;
|
||||
}) {
|
||||
return await prisma.team.findFirst({
|
||||
return await this.prismaClient.team.findFirst({
|
||||
where: {
|
||||
id,
|
||||
parentId,
|
||||
@@ -212,7 +215,7 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findFirstBySlugAndParentSlug({
|
||||
async findFirstBySlugAndParentSlug({
|
||||
slug,
|
||||
parentSlug,
|
||||
select = teamSelect,
|
||||
@@ -221,7 +224,7 @@ export class TeamRepository {
|
||||
parentSlug: string | null;
|
||||
select?: Prisma.TeamSelect;
|
||||
}) {
|
||||
return await prisma.team.findFirst({
|
||||
return await this.prismaClient.team.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
parent: parentSlug ? whereClauseForOrgWithSlugOrRequestedSlug(parentSlug) : null,
|
||||
@@ -230,8 +233,8 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async deleteById({ id }: { id: number }) {
|
||||
const deletedTeam = await prisma.$transaction(async (tx) => {
|
||||
async deleteById({ id }: { id: number }) {
|
||||
const deletedTeam = await this.prismaClient.$transaction(async (tx) => {
|
||||
await tx.eventType.deleteMany({
|
||||
where: {
|
||||
teamId: id,
|
||||
@@ -258,8 +261,8 @@ export class TeamRepository {
|
||||
return deletedTeam;
|
||||
}
|
||||
|
||||
static async findTeamWithMembers(teamId: number) {
|
||||
return await prisma.team.findUnique({
|
||||
async findTeamWithMembers(teamId: number) {
|
||||
return await this.prismaClient.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
members: {
|
||||
@@ -275,8 +278,8 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findTeamsByUserId({ userId, includeOrgs }: { userId: number; includeOrgs?: boolean }) {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
async findTeamsByUserId({ userId, includeOrgs }: { userId: number; includeOrgs?: boolean }) {
|
||||
const memberships = await this.prismaClient.membership.findMany({
|
||||
where: {
|
||||
// Show all the teams this user belongs to regardless of the team being part of the user's org or not
|
||||
// We don't want to restrict in the listing here. If we need to restrict a situation where a user is part of the org along with being part of a non-org team, we should do that instead of filtering out from here
|
||||
@@ -317,8 +320,8 @@ export class TeamRepository {
|
||||
}));
|
||||
}
|
||||
|
||||
static async findTeamWithOrganizationSettings(teamId: number) {
|
||||
return await prisma.team.findUnique({
|
||||
async findTeamWithOrganizationSettings(teamId: number) {
|
||||
return await this.prismaClient.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
parent: {
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("UserRepository", () => {
|
||||
|
||||
describe("create", () => {
|
||||
test("Should create a user without a password", async () => {
|
||||
const user = await UserRepository.create({
|
||||
const user = await new UserRepository(prismock).create({
|
||||
username: "test",
|
||||
email: "test@example.com",
|
||||
organizationId: null,
|
||||
@@ -50,7 +50,7 @@ describe("UserRepository", () => {
|
||||
});
|
||||
|
||||
test("If locked param is passed, user should be locked", async () => {
|
||||
const user = await UserRepository.create({
|
||||
const user = await new UserRepository(prismock).create({
|
||||
username: "test",
|
||||
email: "test@example.com",
|
||||
organizationId: null,
|
||||
@@ -78,7 +78,7 @@ describe("UserRepository", () => {
|
||||
const organizationId = 123;
|
||||
const username = "test";
|
||||
|
||||
const user = await UserRepository.create({
|
||||
const user = await new UserRepository(prismock).create({
|
||||
username,
|
||||
email: "test@example.com",
|
||||
organizationId,
|
||||
|
||||
@@ -4,9 +4,9 @@ import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizatio
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { User as UserType } from "@calcom/prisma/client";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { Prisma, User as UserType } from "@calcom/prisma/client";
|
||||
import type { CreationSource } from "@calcom/prisma/enums";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
@@ -23,6 +23,46 @@ export type { UserWithLegacySelectedCalendars } from "../withSelectedCalendars";
|
||||
export { withSelectedCalendars };
|
||||
export type UserAdminTeams = number[];
|
||||
|
||||
export type SessionUser = {
|
||||
id: number;
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
email: string;
|
||||
emailVerified: Date | null;
|
||||
bio: string | null;
|
||||
avatarUrl: string | null;
|
||||
timeZone: string;
|
||||
weekStart: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
defaultScheduleId: number | null;
|
||||
bufferTime: number;
|
||||
theme: string | null;
|
||||
appTheme: string | null;
|
||||
createdDate: Date;
|
||||
hideBranding: boolean;
|
||||
twoFactorEnabled: boolean;
|
||||
disableImpersonation: boolean;
|
||||
identityProvider: string | null;
|
||||
identityProviderId: string | null;
|
||||
brandColor: string | null;
|
||||
darkBrandColor: string | null;
|
||||
movedToProfileId: number | null;
|
||||
completedOnboarding: boolean;
|
||||
destinationCalendar: any;
|
||||
locale: string;
|
||||
timeFormat: number | null;
|
||||
trialEndsAt: Date | null;
|
||||
metadata: any;
|
||||
role: string;
|
||||
allowDynamicBooking: boolean;
|
||||
allowSEOIndexing: boolean;
|
||||
receiveMonthlyDigestEmail: boolean;
|
||||
profiles: any[];
|
||||
allSelectedCalendars: any[];
|
||||
userLevelSelectedCalendars: any[];
|
||||
};
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[repository/user]"] });
|
||||
|
||||
export const ORGANIZATION_ID_UNKNOWN = "ORGANIZATION_ID_UNKNOWN";
|
||||
@@ -80,8 +120,10 @@ const userSelect = {
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
export class UserRepository {
|
||||
static async findTeamsByUserId({ userId }: { userId: UserType["id"] }) {
|
||||
const teamMemberships = await prisma.membership.findMany({
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async findTeamsByUserId({ userId }: { userId: UserType["id"] }) {
|
||||
const teamMemberships = await this.prismaClient.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
@@ -103,8 +145,8 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static async findOrganizations({ userId }: { userId: UserType["id"] }) {
|
||||
const { acceptedTeamMemberships } = await UserRepository.findTeamsByUserId({
|
||||
async findOrganizations({ userId }: { userId: UserType["id"] }) {
|
||||
const { acceptedTeamMemberships } = await this.findTeamsByUserId({
|
||||
userId,
|
||||
});
|
||||
|
||||
@@ -122,20 +164,14 @@ export class UserRepository {
|
||||
/**
|
||||
* It is aware of the fact that a user can be part of multiple organizations.
|
||||
*/
|
||||
static async findUsersByUsername({
|
||||
orgSlug,
|
||||
usernameList,
|
||||
}: {
|
||||
orgSlug: string | null;
|
||||
usernameList: string[];
|
||||
}) {
|
||||
const { where, profiles } = await UserRepository._getWhereClauseForFindingUsersByUsername({
|
||||
async findUsersByUsername({ orgSlug, usernameList }: { orgSlug: string | null; usernameList: string[] }) {
|
||||
const { where, profiles } = await this._getWhereClauseForFindingUsersByUsername({
|
||||
orgSlug,
|
||||
usernameList,
|
||||
});
|
||||
|
||||
return (
|
||||
await prisma.user.findMany({
|
||||
await this.prismaClient.user.findMany({
|
||||
select: userSelect,
|
||||
where,
|
||||
})
|
||||
@@ -161,9 +197,9 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findPlatformMembersByUsernames({ usernameList }: { usernameList: string[] }) {
|
||||
async findPlatformMembersByUsernames({ usernameList }: { usernameList: string[] }) {
|
||||
return (
|
||||
await prisma.user.findMany({
|
||||
await this.prismaClient.user.findMany({
|
||||
select: userSelect,
|
||||
where: {
|
||||
username: {
|
||||
@@ -187,7 +223,7 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async _getWhereClauseForFindingUsersByUsername({
|
||||
async _getWhereClauseForFindingUsersByUsername({
|
||||
orgSlug,
|
||||
usernameList,
|
||||
}: {
|
||||
@@ -229,8 +265,8 @@ export class UserRepository {
|
||||
return { where, profiles };
|
||||
}
|
||||
|
||||
static async findByEmail({ email }: { email: string }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
async findByEmail({ email }: { email: string }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
@@ -239,8 +275,8 @@ export class UserRepository {
|
||||
return user;
|
||||
}
|
||||
|
||||
static async findByEmailAndIncludeProfilesAndPassword({ email }: { email: string }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
async findByEmailAndIncludeProfilesAndPassword({ email }: { email: string }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
@@ -280,8 +316,8 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static async findById({ id }: { id: number }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
async findById({ id }: { id: number }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -297,8 +333,8 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static async findByIds({ ids }: { ids: number[] }) {
|
||||
return prisma.user.findMany({
|
||||
async findByIds({ ids }: { ids: number[] }) {
|
||||
return this.prismaClient.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids,
|
||||
@@ -308,20 +344,20 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findByIdOrThrow({ id }: { id: number }) {
|
||||
const user = await UserRepository.findById({ id });
|
||||
async findByIdOrThrow({ id }: { id: number }) {
|
||||
const user = await this.findById({ id });
|
||||
if (!user) {
|
||||
throw new Error(`User with id ${id} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
static async findManyByOrganization({ organizationId }: { organizationId: number }) {
|
||||
async findManyByOrganization({ organizationId }: { organizationId: number }) {
|
||||
const profiles = await ProfileRepository.findManyForOrg({ organizationId });
|
||||
return profiles.map((profile) => profile.user);
|
||||
}
|
||||
|
||||
static isAMemberOfOrganization({
|
||||
isAMemberOfOrganization({
|
||||
user,
|
||||
organizationId,
|
||||
}: {
|
||||
@@ -331,7 +367,7 @@ export class UserRepository {
|
||||
return user.profiles.some((profile) => profile.organizationId === organizationId);
|
||||
}
|
||||
|
||||
static async findIfAMemberOfSomeOrganization({ user }: { user: { id: number } }) {
|
||||
async findIfAMemberOfSomeOrganization({ user }: { user: { id: number } }) {
|
||||
return !!(
|
||||
await ProfileRepository.findManyForUser({
|
||||
id: user.id,
|
||||
@@ -339,7 +375,7 @@ export class UserRepository {
|
||||
).length;
|
||||
}
|
||||
|
||||
static isMigratedToOrganization({
|
||||
isMigratedToOrganization({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
@@ -351,11 +387,11 @@ export class UserRepository {
|
||||
return !!user.metadata?.migratedToOrgFrom;
|
||||
}
|
||||
|
||||
static async isMovedToAProfile({ user }: { user: Pick<UserType, "movedToProfileId"> }) {
|
||||
async isMovedToAProfile({ user }: { user: Pick<UserType, "movedToProfileId"> }) {
|
||||
return !!user.movedToProfileId;
|
||||
}
|
||||
|
||||
static async enrichUserWithTheProfile<T extends { username: string | null; id: number }>({
|
||||
async enrichUserWithTheProfile<T extends { username: string | null; id: number }>({
|
||||
user,
|
||||
upId,
|
||||
}: {
|
||||
@@ -382,7 +418,13 @@ export class UserRepository {
|
||||
* 2. While dealing with a User that has been moved to a Profile i.e. he was invited to an organization when he was an existing user.
|
||||
* 3. We haven't added profileId to all the entities, so they aren't aware of which profile they belong to. So, we still mostly use this function to enrich the user with its profile.
|
||||
*/
|
||||
static async enrichUserWithItsProfile<T extends { id: number; username: string | null }>({
|
||||
async enrichUserWithItsProfile<
|
||||
T extends {
|
||||
id: number;
|
||||
username: string | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
>({
|
||||
user,
|
||||
}: {
|
||||
user: T;
|
||||
@@ -420,7 +462,7 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static async enrichUsersWithTheirProfiles<T extends { id: number; username: string | null }>(
|
||||
async enrichUsersWithTheirProfiles<T extends { id: number; username: string | null }>(
|
||||
users: T[]
|
||||
): Promise<
|
||||
Array<
|
||||
@@ -479,7 +521,7 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static enrichUserWithItsProfileBuiltFromUser<T extends { id: number; username: string | null }>({
|
||||
enrichUserWithItsProfileBuiltFromUser<T extends { id: number; username: string | null }>({
|
||||
user,
|
||||
}: {
|
||||
user: T;
|
||||
@@ -495,7 +537,7 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static async enrichEntityWithProfile<
|
||||
async enrichEntityWithProfile<
|
||||
T extends
|
||||
| {
|
||||
profile: {
|
||||
@@ -554,7 +596,7 @@ export class UserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
static async updateWhereId({
|
||||
async updateWhereId({
|
||||
whereId,
|
||||
data,
|
||||
}: {
|
||||
@@ -563,7 +605,7 @@ export class UserRepository {
|
||||
movedToProfileId?: number | null;
|
||||
};
|
||||
}) {
|
||||
return prisma.user.update({
|
||||
return this.prismaClient.user.update({
|
||||
where: {
|
||||
id: whereId,
|
||||
},
|
||||
@@ -579,7 +621,7 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async create(
|
||||
async create(
|
||||
data: Omit<Prisma.UserCreateInput, "password" | "organization" | "movedToProfile"> & {
|
||||
username: string;
|
||||
hashedPassword?: string;
|
||||
@@ -595,7 +637,7 @@ export class UserRepository {
|
||||
const t = await getTranslation("en", "common");
|
||||
const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
const user = await this.prismaClient.user.create({
|
||||
data: {
|
||||
username,
|
||||
email: email,
|
||||
@@ -635,8 +677,8 @@ export class UserRepository {
|
||||
|
||||
return user;
|
||||
}
|
||||
static async getUserAdminTeams(userId: number) {
|
||||
return prisma.user.findUnique({
|
||||
async getUserAdminTeams({ userId }: { userId: number }) {
|
||||
return await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
@@ -687,7 +729,7 @@ export class UserRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
static async isAdminOfTeamOrParentOrg({ userId, teamId }: { userId: number; teamId: number }) {
|
||||
async isAdminOfTeamOrParentOrg({ userId, teamId }: { userId: number; teamId: number }) {
|
||||
const membershipQuery = {
|
||||
members: {
|
||||
some: {
|
||||
@@ -696,7 +738,7 @@ export class UserRepository {
|
||||
},
|
||||
},
|
||||
};
|
||||
const teams = await prisma.team.findMany({
|
||||
const teams = await this.prismaClient.team.findMany({
|
||||
where: {
|
||||
id: teamId,
|
||||
OR: [
|
||||
@@ -712,8 +754,8 @@ export class UserRepository {
|
||||
});
|
||||
return !!teams.length;
|
||||
}
|
||||
static async isAdminOrOwnerOfTeam({ userId, teamId }: { userId: number; teamId: number }) {
|
||||
const isAdminOrOwnerOfTeam = await prisma.membership.findUnique({
|
||||
async isAdminOrOwnerOfTeam({ userId, teamId }: { userId: number; teamId: number }) {
|
||||
const isAdminOrOwnerOfTeam = await this.prismaClient.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId,
|
||||
@@ -728,8 +770,8 @@ export class UserRepository {
|
||||
});
|
||||
return !!isAdminOrOwnerOfTeam;
|
||||
}
|
||||
static async getTimeZoneAndDefaultScheduleId({ userId }: { userId: number }) {
|
||||
return await prisma.user.findUnique({
|
||||
async getTimeZoneAndDefaultScheduleId({ userId }: { userId: number }) {
|
||||
return await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
@@ -740,16 +782,16 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async adminFindById(userId: number) {
|
||||
return await prisma.user.findUniqueOrThrow({
|
||||
async adminFindById(userId: number) {
|
||||
return await this.prismaClient.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async findUserTeams({ id }: { id: number }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
async findUserTeams({ id }: { id: number }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -776,10 +818,10 @@ export class UserRepository {
|
||||
return user;
|
||||
}
|
||||
|
||||
static async updateAvatar({ id, avatarUrl }: { id: number; avatarUrl: string }) {
|
||||
async updateAvatar({ id, avatarUrl }: { id: number; avatarUrl: string }) {
|
||||
// Using updateMany here since if the user already has a profile it would throw an error
|
||||
// because no records were found to update the profile picture
|
||||
await prisma.user.updateMany({
|
||||
await this.prismaClient.user.updateMany({
|
||||
where: {
|
||||
id,
|
||||
avatarUrl: {
|
||||
@@ -791,8 +833,8 @@ export class UserRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
static async findUserWithCredentials({ id }: { id: number }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
async findUserWithCredentials({ id }: { id: number }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -817,8 +859,8 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static async findUnlockedUserForSession({ userId }: { userId: number }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
async findUnlockedUserForSession({ userId }: { userId: number }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
// Locked users can't login
|
||||
@@ -877,8 +919,8 @@ export class UserRepository {
|
||||
return withSelectedCalendars(user);
|
||||
}
|
||||
|
||||
static async getUserStats({ userId }: { userId: number }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
async getUserStats({ userId }: { userId: number }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
@@ -927,8 +969,8 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static async findManyByIdsIncludeDestinationAndSelectedCalendars({ ids }: { ids: number[] }) {
|
||||
const users = await prisma.user.findMany({
|
||||
async findManyByIdsIncludeDestinationAndSelectedCalendars({ ids }: { ids: number[] }) {
|
||||
const users = await this.prismaClient.user.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: {
|
||||
selectedCalendars: true,
|
||||
@@ -938,7 +980,7 @@ export class UserRepository {
|
||||
return users.map(withSelectedCalendars);
|
||||
}
|
||||
|
||||
static async updateStripeCustomerId({
|
||||
async updateStripeCustomerId({
|
||||
id,
|
||||
stripeCustomerId,
|
||||
existingMetadata,
|
||||
@@ -947,39 +989,33 @@ export class UserRepository {
|
||||
stripeCustomerId: string;
|
||||
existingMetadata: z.infer<typeof userMetadata>;
|
||||
}) {
|
||||
return prisma.user.update({
|
||||
return this.prismaClient.user.update({
|
||||
where: { id },
|
||||
data: { metadata: { ...existingMetadata, stripeCustomerId } },
|
||||
});
|
||||
}
|
||||
|
||||
static async updateWhitelistWorkflows({
|
||||
id,
|
||||
whitelistWorkflows,
|
||||
}: {
|
||||
id: number;
|
||||
whitelistWorkflows: boolean;
|
||||
}) {
|
||||
return prisma.user.update({
|
||||
async updateWhitelistWorkflows({ id, whitelistWorkflows }: { id: number; whitelistWorkflows: boolean }) {
|
||||
return this.prismaClient.user.update({
|
||||
where: { id },
|
||||
data: { whitelistWorkflows },
|
||||
});
|
||||
}
|
||||
|
||||
static async findManyUsersForDynamicEventType({
|
||||
async findManyUsersForDynamicEventType({
|
||||
currentOrgDomain,
|
||||
usernameList,
|
||||
}: {
|
||||
currentOrgDomain: string | null;
|
||||
usernameList: string[];
|
||||
}) {
|
||||
const { where } = await UserRepository._getWhereClauseForFindingUsersByUsername({
|
||||
const { where } = await this._getWhereClauseForFindingUsersByUsername({
|
||||
orgSlug: currentOrgDomain,
|
||||
usernameList,
|
||||
});
|
||||
|
||||
// TODO: Should be moved to UserRepository
|
||||
return prisma.user.findMany({
|
||||
return this.prismaClient.user.findMany({
|
||||
where,
|
||||
select: {
|
||||
allowDynamicBooking: true,
|
||||
|
||||
@@ -35,14 +35,17 @@ const mockTeamBilling = {
|
||||
};
|
||||
|
||||
// Mock implementations that modify the in-memory database
|
||||
vi.mocked(TeamRepository.deleteById).mockImplementation(async ({ id }) => {
|
||||
const team = database.teams.get(id);
|
||||
if (team) {
|
||||
database.teams.delete(id);
|
||||
return team;
|
||||
}
|
||||
throw new Error(`Team with id ${id} not found`);
|
||||
});
|
||||
const mockTeamRepo = {
|
||||
deleteById: vi.fn().mockImplementation(async ({ id }) => {
|
||||
const team = database.teams.get(id);
|
||||
if (team) {
|
||||
database.teams.delete(id);
|
||||
return team;
|
||||
}
|
||||
throw new Error(`Team with id ${id} not found`);
|
||||
}),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
vi.mocked(deleteDomain).mockImplementation(async (slug) => {
|
||||
database.domains.delete(slug);
|
||||
|
||||
@@ -47,14 +47,17 @@ describe("TeamService", () => {
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
vi.mocked(TeamRepository.deleteById).mockResolvedValue(mockDeletedTeam);
|
||||
const mockTeamRepo = {
|
||||
deleteById: vi.fn().mockResolvedValue(mockDeletedTeam),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
const result = await TeamService.delete({ id: 1 });
|
||||
|
||||
expect(TeamBilling.findAndInit).toHaveBeenCalledWith(1);
|
||||
expect(mockTeamBilling.cancel).toHaveBeenCalled();
|
||||
expect(WorkflowService.deleteWorkflowRemindersOfRemovedTeam).toHaveBeenCalledWith(1);
|
||||
expect(TeamRepository.deleteById).toHaveBeenCalledWith({ id: 1 });
|
||||
expect(mockTeamRepo.deleteById).toHaveBeenCalledWith({ id: 1 });
|
||||
expect(deleteDomain).toHaveBeenCalledWith("deleted-team");
|
||||
expect(result).toEqual(mockDeletedTeam);
|
||||
});
|
||||
|
||||
@@ -144,7 +144,8 @@ export class InsightsBookingService {
|
||||
options: Extract<InsightsBookingServiceOptions, { scope: "org" }>
|
||||
): Promise<Prisma.BookingTimeStatusDenormalizedWhereInput> {
|
||||
// Get all teams from the organization
|
||||
const teamsFromOrg = await TeamRepository.findAllByParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const teamsFromOrg = await teamRepo.findAllByParentId({
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
});
|
||||
@@ -183,7 +184,8 @@ export class InsightsBookingService {
|
||||
private async buildTeamAuthorizationCondition(
|
||||
options: Extract<InsightsBookingServiceOptions, { scope: "team" }>
|
||||
): Promise<Prisma.BookingTimeStatusDenormalizedWhereInput> {
|
||||
const childTeamOfOrg = await TeamRepository.findByIdAndParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const childTeamOfOrg = await teamRepo.findByIdAndParentId({
|
||||
id: options.teamId,
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
|
||||
@@ -126,7 +126,8 @@ export class InsightsRoutingService {
|
||||
options: Extract<InsightsRoutingServiceOptions, { scope: "org" }>
|
||||
): Promise<Prisma.RoutingFormResponseDenormalizedWhereInput> {
|
||||
// Get all teams from the organization
|
||||
const teamsFromOrg = await TeamRepository.findAllByParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const teamsFromOrg = await teamRepo.findAllByParentId({
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
});
|
||||
@@ -141,7 +142,8 @@ export class InsightsRoutingService {
|
||||
private async buildTeamAuthorizationCondition(
|
||||
options: Extract<InsightsRoutingServiceOptions, { scope: "team" }>
|
||||
): Promise<Prisma.RoutingFormResponseDenormalizedWhereInput> {
|
||||
const childTeamOfOrg = await TeamRepository.findByIdAndParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const childTeamOfOrg = await teamRepo.findByIdAndParentId({
|
||||
id: options.teamId,
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
|
||||
@@ -34,7 +34,8 @@ export class TeamService {
|
||||
}
|
||||
|
||||
// Step 3: Delete the team from the database. This is the core "commit" point.
|
||||
const deletedTeam = await TeamRepository.deleteById({ id });
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
const deletedTeam = await teamRepo.deleteById({ id });
|
||||
|
||||
// Step 4: Clean up any final, non-critical external state.
|
||||
if (deletedTeam && deletedTeam.isOrganization && deletedTeam.slug) {
|
||||
|
||||
@@ -21,11 +21,11 @@ vi.mock("@calcom/features/auth/lib/hashPassword", () => ({
|
||||
hashPassword: vi.fn().mockResolvedValue("hashed-password"),
|
||||
}));
|
||||
|
||||
vi.mock("../repository/user", async () => {
|
||||
vi.mock("../repository/user", () => {
|
||||
return {
|
||||
UserRepository: {
|
||||
UserRepository: vi.fn().mockImplementation(() => ({
|
||||
create: vi.fn(),
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -48,15 +48,25 @@ describe("UserCreationService", () => {
|
||||
});
|
||||
|
||||
test("should create user", async () => {
|
||||
vi.spyOn(UserRepository, "create").mockResolvedValue({
|
||||
const mockCreate = vi.fn().mockResolvedValue({
|
||||
username: "test",
|
||||
locked: false,
|
||||
organizationId: null,
|
||||
} as any);
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
create: mockCreate,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const user = await UserCreationService.createUser({ data: mockUserData });
|
||||
|
||||
expect(UserRepository.create).toHaveBeenCalledWith(
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
username: "test",
|
||||
locked: false,
|
||||
@@ -70,9 +80,25 @@ describe("UserCreationService", () => {
|
||||
test("should lock user when email is in watchlist", async () => {
|
||||
vi.mocked(checkIfEmailIsBlockedInWatchlistController).mockResolvedValue(true);
|
||||
|
||||
const mockCreate = vi.fn().mockResolvedValue({
|
||||
username: "test",
|
||||
locked: true,
|
||||
organizationId: null,
|
||||
} as any);
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
create: mockCreate,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const user = await UserCreationService.createUser({ data: mockUserData });
|
||||
|
||||
expect(UserRepository.create).toHaveBeenCalledWith(
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
locked: true,
|
||||
})
|
||||
@@ -85,12 +111,28 @@ describe("UserCreationService", () => {
|
||||
const mockPassword = "password";
|
||||
vi.mocked(hashPassword).mockResolvedValue("hashed_password");
|
||||
|
||||
const mockCreate = vi.fn().mockResolvedValue({
|
||||
username: "test",
|
||||
locked: false,
|
||||
organizationId: null,
|
||||
} as any);
|
||||
|
||||
const mockUserRepository = vi.mocked(UserRepository);
|
||||
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
|
||||
mockUserRepository.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
create: mockCreate,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
const user = await UserCreationService.createUser({
|
||||
data: { ...mockUserData, password: mockPassword },
|
||||
});
|
||||
|
||||
expect(hashPassword).toHaveBeenCalledWith(mockPassword);
|
||||
expect(UserRepository.create).toHaveBeenCalledWith(
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
hashedPassword: "hashed_password",
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CreationSource, UserPermissionRole, IdentityProvider } from "@calcom/prisma/enums";
|
||||
|
||||
import slugify from "../../slugify";
|
||||
@@ -37,7 +38,8 @@ export class UserCreationService {
|
||||
|
||||
const hashedPassword = password ? await hashPassword(password) : null;
|
||||
|
||||
const user = await UserRepository.create({
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const user = await userRepo.create({
|
||||
...data,
|
||||
username: slugify(username),
|
||||
...(hashedPassword && { hashedPassword }),
|
||||
|
||||
@@ -8,7 +8,7 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-[24px] w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -80,7 +80,7 @@ function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
|
||||
constraints: [],
|
||||
validator: {
|
||||
validate(_: unknown, args: ValidationArguments) {
|
||||
const obj = args.object as Attendee;
|
||||
const obj = args.object as CreateBookingAttendee;
|
||||
|
||||
const hasPhoneNumber = !!obj.phoneNumber;
|
||||
const hasEmail = !!obj.email;
|
||||
@@ -95,7 +95,7 @@ function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
|
||||
}
|
||||
|
||||
@RequireEmailOrPhone()
|
||||
class Attendee {
|
||||
class CreateBookingAttendee {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description: "The name of the attendee.",
|
||||
@@ -222,13 +222,13 @@ export class CreateBookingInput_2024_08_13 {
|
||||
start!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Attendee,
|
||||
type: CreateBookingAttendee,
|
||||
description: "The attendee's details.",
|
||||
})
|
||||
@IsDefined()
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
attendee!: Attendee;
|
||||
@Type(() => CreateBookingAttendee)
|
||||
attendee!: CreateBookingAttendee;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: Object,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsOptional, IsBoolean, IsEmail, IsArray, ArrayMinSize, ValidateNested } from "class-validator";
|
||||
|
||||
class Attendee {
|
||||
class MarkAbsentAttendee {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
@@ -21,8 +21,8 @@ export class MarkAbsentBookingInput_2024_08_13 {
|
||||
@ArrayMinSize(1)
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@Type(() => MarkAbsentAttendee)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Attendee] })
|
||||
attendees?: Attendee[];
|
||||
@ApiPropertyOptional({ type: [MarkAbsentAttendee] })
|
||||
attendees?: MarkAbsentAttendee[];
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import type { BookingLanguageType } from "../inputs/language";
|
||||
import { BookingLanguage } from "../inputs/language";
|
||||
|
||||
class Attendee {
|
||||
class BookingAttendee {
|
||||
@ApiProperty({ type: String, example: "John Doe" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
@@ -52,7 +52,7 @@ class Attendee {
|
||||
phoneNumber?: string;
|
||||
}
|
||||
|
||||
export class SeatedAttendee extends Attendee {
|
||||
export class SeatedAttendee extends BookingAttendee {
|
||||
@ApiProperty({ type: String, example: "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
@@ -264,11 +264,11 @@ class BaseBookingOutput_2024_08_13 {
|
||||
}
|
||||
|
||||
export class BookingOutput_2024_08_13 extends BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
@ApiProperty({ type: [BookingAttendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@Type(() => BookingAttendee)
|
||||
@Expose()
|
||||
attendees!: Attendee[];
|
||||
attendees!: BookingAttendee[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,3 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { AnyRouter } from "@trpc/server";
|
||||
import { createNextApiHandler as _createNextApiHandler } from "@trpc/server/adapters/next";
|
||||
|
||||
@@ -43,9 +41,6 @@ export function createNextApiHandler(router: AnyRouter, isPublic = false, namesp
|
||||
headers: {},
|
||||
};
|
||||
|
||||
const timezone = z.string().safeParse(ctx.req?.headers["x-vercel-ip-timezone"]);
|
||||
if (timezone.success) defaultHeaders.headers["x-cal-timezone"] = timezone.data;
|
||||
|
||||
// We need all these conditions to be true to set cache headers
|
||||
if (!(isPublic && allOk && isQuery)) return defaultHeaders;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -24,7 +25,8 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
||||
return null;
|
||||
}
|
||||
|
||||
const userFromDb = await UserRepository.findUnlockedUserForSession({ userId: session.user.id });
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const userFromDb = await userRepo.findUnlockedUserForSession({ userId: session.user.id });
|
||||
|
||||
// some hacks to make sure `username` and `email` are never inferred as `null`
|
||||
if (!userFromDb) {
|
||||
@@ -33,7 +35,7 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
||||
|
||||
const upId = session.upId;
|
||||
|
||||
const user = await UserRepository.enrichUserWithTheProfile({
|
||||
const user = await userRepo.enrichUserWithTheProfile({
|
||||
user: userFromDb,
|
||||
upId,
|
||||
});
|
||||
@@ -54,7 +56,7 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
||||
|
||||
const locale = user?.locale ?? ctx.locale;
|
||||
const { members = [], ..._organization } = user.profile?.organization || {};
|
||||
const isOrgAdmin = members.some((member) => ["OWNER", "ADMIN"].includes(member.role));
|
||||
const isOrgAdmin = members.some((member: any) => ["OWNER", "ADMIN"].includes(member.role));
|
||||
|
||||
if (isOrgAdmin) {
|
||||
logger.debug("User is an org admin", safeStringify({ userId: user.id }));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user