diff --git a/apps/web/app/future/availability/[schedule]/page.tsx b/apps/web/app/future/availability/[schedule]/page.tsx index 96c99517ab..88588d9de3 100644 --- a/apps/web/app/future/availability/[schedule]/page.tsx +++ b/apps/web/app/future/availability/[schedule]/page.tsx @@ -1,8 +1,13 @@ import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { getServerSession } from "next-auth"; +import { notFound } from "next/navigation"; +import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options"; import { ScheduleRepository } from "@calcom/lib/server/repository/schedule"; +import { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule"; +import { UserRepository } from "@calcom/lib/server/repository/user"; import { AvailabilitySettingsWebWrapper } from "~/availability/[schedule]/schedule-view"; @@ -23,4 +28,50 @@ export const generateMetadata = async ({ params }: PageProps) => { ); }; -export default WithLayout({ getLayout: null, Page: AvailabilitySettingsWebWrapper }); +const Page = async ({ params }: PageProps) => { + if (params?.schedule) { + notFound(); + } + const scheduleId = Number(params.schedule); + + const session = await getServerSession(AUTH_OPTIONS); + const userId = session?.user?.id; + if (!userId) { + notFound(); + } + + let userData, schedule, travelSchedules; + + try { + userData = await UserRepository.findById({ + id: userId, + select: { + timeZone: true, + defaultScheduleId: true, + }, + }); + if (!userData?.timeZone || !userData?.defaultScheduleId) { + throw new Error("timeZone and defaultScheduleId not found"); + } + } catch (e) { + notFound(); + } + + try { + schedule = await ScheduleRepository.findDetailedScheduleById({ + scheduleId, + isManagedEventType: false, + userId, + timeZone: userData.timeZone, + defaultScheduleId: userData.defaultScheduleId, + }); + } catch (e) {} + + try { + travelSchedules = await TravelScheduleRepository.findTravelSchedulesByUserId(userId); + } catch (e) {} + + return ; +}; + +export default WithLayout({ ServerPage: Page }); diff --git a/apps/web/app/future/availability/page.tsx b/apps/web/app/future/availability/page.tsx index f793a90fec..c75021f80a 100644 --- a/apps/web/app/future/availability/page.tsx +++ b/apps/web/app/future/availability/page.tsx @@ -1,5 +1,10 @@ import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { getServerSession } from "next-auth"; +import { notFound } from "next/navigation"; + +import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options"; +import { OrganizationRepository } from "@calcom/lib/server/repository/organization"; import AvailabilityPage from "~/availability/availability-view"; @@ -10,4 +15,23 @@ export const generateMetadata = async () => { ); }; -export default WithLayout({ getLayout: null, Page: AvailabilityPage }); +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + const userId = session?.user?.id; + const orgId = session?.user?.org?.id; + if (!userId || !orgId) { + notFound(); + } + + try { + const currentOrg = await OrganizationRepository.findCurrentOrg({ + orgId, + userId, + }); + return ; + } catch { + notFound(); + } +}; + +export default WithLayout({ ServerPage: Page }); diff --git a/apps/web/modules/availability/[schedule]/schedule-view.tsx b/apps/web/modules/availability/[schedule]/schedule-view.tsx index 03fc0108c3..4876b29f47 100644 --- a/apps/web/modules/availability/[schedule]/schedule-view.tsx +++ b/apps/web/modules/availability/[schedule]/schedule-view.tsx @@ -8,11 +8,21 @@ import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; +import type { ScheduleRepository } from "@calcom/lib/server/repository/schedule"; +import type { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { showToast } from "@calcom/ui"; -export const AvailabilitySettingsWebWrapper = () => { +type PageProps = { + schedule?: Awaited>; + travelSchedules?: Awaited>; +}; + +export const AvailabilitySettingsWebWrapper = ({ + schedule: scheduleProp, + travelSchedules: travelSchedulesProp, +}: PageProps) => { const searchParams = useCompatSearchParams(); const { t } = useLocale(); const router = useRouter(); @@ -21,14 +31,19 @@ export const AvailabilitySettingsWebWrapper = () => { const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1; const fromEventType = searchParams?.get("fromEventType"); const { timeFormat } = me.data || { timeFormat: null }; - const { data: schedule, isPending } = trpc.viewer.availability.schedule.get.useQuery( + const { data: scheduleData, isPending: isFetchingPending } = trpc.viewer.availability.schedule.get.useQuery( { scheduleId }, { - enabled: !!scheduleId, + enabled: !!scheduleId && !scheduleProp, } ); + const isPending = isFetchingPending && !scheduleProp; + const schedule = scheduleProp ?? scheduleData; - const { data: travelSchedules } = trpc.viewer.getTravelSchedules.useQuery(); + const { data: travelSchedulesData } = trpc.viewer.getTravelSchedules.useQuery(undefined, { + enabled: !travelSchedulesProp, + }); + const travelSchedules = travelSchedulesProp ?? travelSchedulesData; const [isBulkUpdateModalOpen, setIsBulkUpdateModalOpen] = useState(false); const bulkUpdateDefaultAvailabilityMutation = diff --git a/apps/web/modules/availability/availability-view.tsx b/apps/web/modules/availability/availability-view.tsx index 378ed5c28a..fad17f183b 100644 --- a/apps/web/modules/availability/availability-view.tsx +++ b/apps/web/modules/availability/availability-view.tsx @@ -13,6 +13,7 @@ import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/compone import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; +import type { OrganizationRepository } from "@calcom/lib/server/repository/organization"; import { MembershipRole } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; @@ -170,13 +171,18 @@ function AvailabilityListWithQuery() { ); } -export default function AvailabilityPage() { +type PageProps = { + currentOrg?: Awaited> | null; +}; + +export default function AvailabilityPage({ currentOrg }: PageProps) { const { t } = useLocale(); const searchParams = useCompatSearchParams(); const router = useRouter(); const pathname = usePathname(); const me = useMeQuery(); - const { data } = trpc.viewer.organizations.listCurrent.useQuery(); + const { data: _data } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { enabled: !currentOrg }); + const data = currentOrg ?? _data; // Get a new searchParams string by merging the current // searchParams with a provided key/value pair diff --git a/packages/lib/hasEditPermissionForUser.ts b/packages/lib/hasEditPermissionForUser.ts index 6ba4e49df0..5da76db06d 100644 --- a/packages/lib/hasEditPermissionForUser.ts +++ b/packages/lib/hasEditPermissionForUser.ts @@ -38,19 +38,20 @@ export async function hasEditPermissionForUserID({ ctx, input }: InputOptions) { return teamIdOverlaps; } -export async function hasReadPermissionsForUserId({ ctx, input }: InputOptions) { - const { user } = ctx; - +export async function hasReadPermissionsForUserId({ + userId, + memberId, +}: InputOptions["input"] & { userId: number }) { const authedUsersTeams = await prisma.membership.findMany({ where: { - userId: user.id, + userId, accepted: true, }, }); const targetUsersTeams = await prisma.membership.findMany({ where: { - userId: input.memberId, + userId: memberId, accepted: true, }, }); diff --git a/packages/lib/server/repository/schedule.ts b/packages/lib/server/repository/schedule.ts index a9e75de317..e44b429583 100644 --- a/packages/lib/server/repository/schedule.ts +++ b/packages/lib/server/repository/schedule.ts @@ -1,4 +1,12 @@ import prisma from "@calcom/prisma"; +import { getDefaultScheduleId } from "@calcom/trpc/server/routers/viewer/availability/util"; + +import { hasReadPermissionsForUserId } from "../../hasEditPermissionForUser"; +import { + transformAvailabilityForAtom, + transformDateOverridesForAtom, + transformWorkingHoursForAtom, +} from "../../schedules"; export class ScheduleRepository { static async findScheduleById({ id }: { id: number }) { @@ -17,4 +25,64 @@ export class ScheduleRepository { return schedule; } + + static async findDetailedScheduleById({ + isManagedEventType, + scheduleId, + userId, + defaultScheduleId, + timeZone: userTimeZone, + }: { + timeZone: string; + userId: number; + defaultScheduleId: number | null; + scheduleId?: number; + isManagedEventType?: boolean; + }) { + const schedule = await prisma.schedule.findUnique({ + where: { + id: scheduleId || (await getDefaultScheduleId(userId, prisma)), + }, + select: { + id: true, + userId: true, + name: true, + availability: true, + timeZone: true, + }, + }); + + if (!schedule) { + throw new Error("Schedule not found"); + } + const isCurrentUserPartOfTeam = hasReadPermissionsForUserId({ memberId: schedule?.userId, userId }); + + const isCurrentUserOwner = schedule?.userId === userId; + + if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) { + throw new Error("UNAUTHORIZED"); + } + + const timeZone = schedule.timeZone || userTimeZone; + + const schedulesCount = await prisma.schedule.count({ + where: { + userId: userId, + }, + }); + // disabling utc casting while fetching WorkingHours + return { + id: schedule.id, + name: schedule.name, + isManaged: schedule.userId !== userId, + workingHours: transformWorkingHoursForAtom(schedule), + schedule: schedule.availability, + availability: transformAvailabilityForAtom(schedule), + timeZone, + dateOverrides: transformDateOverridesForAtom(schedule, timeZone), + isDefault: !scheduleId || defaultScheduleId === schedule.id, + isLastSchedule: schedulesCount <= 1, + readOnly: schedule.userId !== userId && !isManagedEventType, + }; + } } diff --git a/packages/lib/server/repository/travelSchedule.ts b/packages/lib/server/repository/travelSchedule.ts new file mode 100644 index 0000000000..933f01162d --- /dev/null +++ b/packages/lib/server/repository/travelSchedule.ts @@ -0,0 +1,19 @@ +import { prisma } from "@calcom/prisma"; + +export class TravelScheduleRepository { + static async findTravelSchedulesByUserId(userId: number) { + const allTravelSchedules = await prisma.travelSchedule.findMany({ + where: { + userId, + }, + select: { + id: true, + startDate: true, + endDate: true, + timeZone: true, + }, + }); + + return allTravelSchedules; + } +} diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index 8e39cfb36e..91c7a29f6c 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -238,12 +238,12 @@ export class UserRepository { }; } - static async findById({ id }: { id: number }) { + static async findById({ id, select }: { id: number; select?: Prisma.UserSelect }) { const user = await prisma.user.findUnique({ where: { id, }, - select: userSelect, + select: { ...userSelect, ...select }, }); if (!user) { diff --git a/packages/trpc/server/routers/loggedInViewer/getTravelSchedules.handler.ts b/packages/trpc/server/routers/loggedInViewer/getTravelSchedules.handler.ts index a47e454ae5..7f3b1d4768 100644 --- a/packages/trpc/server/routers/loggedInViewer/getTravelSchedules.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/getTravelSchedules.handler.ts @@ -1,4 +1,4 @@ -import { prisma } from "@calcom/prisma"; +import { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; type GetTravelSchedulesOptions = { @@ -8,17 +8,5 @@ type GetTravelSchedulesOptions = { }; export const getTravelSchedulesHandler = async ({ ctx }: GetTravelSchedulesOptions) => { - const allTravelSchedules = await prisma.travelSchedule.findMany({ - where: { - userId: ctx.user.id, - }, - select: { - id: true, - startDate: true, - endDate: true, - timeZone: true, - }, - }); - - return allTravelSchedules; + return await TravelScheduleRepository.findTravelSchedulesByUserId(ctx.user.id); }; diff --git a/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts index 0ede8713f0..affcad719a 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts @@ -1,15 +1,6 @@ -import { - transformAvailabilityForAtom, - transformDateOverridesForAtom, - transformWorkingHoursForAtom, -} from "@calcom/lib"; -import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser"; -import { prisma } from "@calcom/prisma"; - -import { TRPCError } from "@trpc/server"; +import { ScheduleRepository } from "@calcom/lib/server/repository/schedule"; import type { TrpcSessionUser } from "../../../../trpc"; -import { getDefaultScheduleId } from "../util"; import type { TGetInputSchema } from "./get.schema"; type GetOptions = { @@ -20,58 +11,11 @@ type GetOptions = { }; export const getHandler = async ({ ctx, input }: GetOptions) => { - const { user } = ctx; - - const schedule = await prisma.schedule.findUnique({ - where: { - id: input.scheduleId || (await getDefaultScheduleId(user.id, prisma)), - }, - select: { - id: true, - userId: true, - name: true, - availability: true, - timeZone: true, - }, + return await ScheduleRepository.findDetailedScheduleById({ + scheduleId: input.scheduleId, + isManagedEventType: input.isManagedEventType, + userId: ctx.user.id, + timeZone: ctx.user.timeZone, + defaultScheduleId: ctx.user.defaultScheduleId, }); - - if (!schedule) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); - } - const isCurrentUserPartOfTeam = hasReadPermissionsForUserId({ - ctx, - input: { memberId: schedule?.userId }, - }); - - const isCurrentUserOwner = schedule?.userId === user.id; - - if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); - } - - const timeZone = schedule.timeZone || user.timeZone; - - const schedulesCount = await prisma.schedule.count({ - where: { - userId: user.id, - }, - }); - // disabling utc casting while fetching WorkingHours - return { - id: schedule.id, - name: schedule.name, - isManaged: schedule.userId !== user.id, - workingHours: transformWorkingHoursForAtom(schedule), - schedule: schedule.availability, - availability: transformAvailabilityForAtom(schedule), - timeZone, - dateOverrides: transformDateOverridesForAtom(schedule, timeZone), - isDefault: !input.scheduleId || user.defaultScheduleId === schedule.id, - isLastSchedule: schedulesCount <= 1, - readOnly: schedule.userId !== user.id && !input.isManagedEventType, - }; };