fix: fix data expose issue in routing form insights (#18207)

This commit is contained in:
Anik Dhabal Babu
2024-12-16 15:30:25 +01:00
committed by GitHub
parent f8be2b3833
commit 1773aba378
13 changed files with 136 additions and 36 deletions
@@ -56,11 +56,12 @@ function FormCard({ formName, fields }: FormCardProps) {
export function FailedBookingsByField() {
const { filter } = useFilterContext();
const { selectedTeamId, isAll, initialConfig, selectedRoutingFormId } = filter;
const { selectedTeamId, selectedUserId, isAll, initialConfig, selectedRoutingFormId } = filter;
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
const { data } = trpc.viewer.insights.failedBookingsByField.useQuery(
{
userId: selectedUserId ?? undefined,
teamId: selectedTeamId ?? undefined,
isAll: !!isAll,
routingFormId: selectedRoutingFormId ?? undefined,
@@ -33,6 +33,7 @@ import { useFilterContext } from "../context/provider";
interface DownloadButtonProps {
teamId?: number;
userId?: number;
isAll?: boolean;
routingFormId?: string;
dateRange: [Dayjs, Dayjs, string | null];
@@ -41,6 +42,7 @@ interface DownloadButtonProps {
}
function DownloadButton({
userId,
teamId,
isAll,
routingFormId,
@@ -57,6 +59,7 @@ function DownloadButton({
try {
const result = await utils.viewer.insights.routedToPerPeriodCsv.fetch({
userId: userId ?? undefined,
teamId: teamId ?? undefined,
startDate: dateRange[0]?.toISOString() ?? "",
endDate: dateRange[1]?.toISOString() ?? "",
@@ -97,6 +100,7 @@ interface FormCardProps {
onSearchChange: (value: string) => void;
children: ReactNode;
teamId?: number;
userId?: number;
isAll?: boolean;
routingFormId?: string;
dateRange: [Dayjs, Dayjs, string | null];
@@ -109,6 +113,7 @@ function FormCard({
onSearchChange,
children,
teamId,
userId,
isAll,
routingFormId,
dateRange,
@@ -141,6 +146,7 @@ function FormCard({
/>
</div>
<DownloadButton
userId={userId}
teamId={teamId}
isAll={isAll}
routingFormId={routingFormId}
@@ -214,7 +220,7 @@ const getPerformanceBadge = (performance: RoutedToTableRow["performance"], t: TF
export function RoutedToPerPeriod() {
const { t } = useLocale();
const { filter } = useFilterContext();
const { selectedTeamId, isAll, selectedRoutingFormId, dateRange } = filter;
const { selectedTeamId, selectedUserId, isAll, selectedRoutingFormId, dateRange } = filter;
const [selectedPeriod, setSelectedPeriod] = useQueryState("selectedPeriod", {
defaultValue: "perWeek",
});
@@ -233,6 +239,7 @@ export function RoutedToPerPeriod() {
const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } =
trpc.viewer.insights.routedToPerPeriod.useInfiniteQuery(
{
userId: selectedUserId ?? undefined,
teamId: selectedTeamId ?? undefined,
startDate: dateRange[0]?.toISOString() ?? "",
endDate: dateRange[1]?.toISOString() ?? "",
@@ -327,6 +334,7 @@ export function RoutedToPerPeriod() {
onPeriodChange={setSelectedPeriod}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
userId={selectedUserId ?? undefined}
teamId={selectedTeamId ?? undefined}
isAll={isAll}
routingFormId={selectedRoutingFormId ?? undefined}
@@ -369,6 +377,7 @@ export function RoutedToPerPeriod() {
onPeriodChange={setSelectedPeriod}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
userId={selectedUserId ?? undefined}
teamId={selectedTeamId ?? undefined}
isAll={isAll}
routingFormId={selectedRoutingFormId ?? undefined}
@@ -266,8 +266,17 @@ export function RoutingFormResponsesTableContent({
const tableContainerRef = useRef<HTMLDivElement>(null);
const { copyToClipboard } = useCopy();
const { dateRange, selectedTeamId, isAll, initialConfig, selectedRoutingFormId, selectedMemberUserId } =
filter;
const {
dateRange,
selectedTeamId,
isAll,
initialConfig,
selectedRoutingFormId,
selectedMemberUserId,
selectedUserId,
selectedBookingStatus,
selectedRoutingFormFilter,
} = filter;
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
const [startDate, endDate] = dateRange;
@@ -276,6 +285,7 @@ export function RoutingFormResponsesTableContent({
const { data: headers, isLoading: isHeadersLoading } =
trpc.viewer.insights.routingFormResponsesHeaders.useQuery(
{
userId: selectedUserId ?? undefined,
teamId: selectedTeamId ?? undefined,
isAll: isAll ?? false,
routingFormId: selectedRoutingFormId ?? undefined,
@@ -291,7 +301,8 @@ export function RoutingFormResponsesTableContent({
teamId: selectedTeamId,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
userId: selectedMemberUserId ?? undefined,
userId: selectedUserId ?? undefined,
memberUserId: selectedMemberUserId ?? undefined,
isAll: isAll ?? false,
routingFormId: selectedRoutingFormId ?? undefined,
columnFilters,
@@ -36,7 +36,8 @@ export const RoutingKPICards = () => {
eventTypeId: selectedEventTypeId ?? undefined,
isAll,
routingFormId: selectedRoutingFormId ?? undefined,
userId: selectedMemberUserId ?? undefined,
userId: selectedUserId ?? undefined,
memberUserId: selectedMemberUserId ?? undefined,
bookingStatus: selectedBookingStatus ?? undefined,
fieldFilter: selectedRoutingFormFilter ?? undefined,
},
@@ -96,6 +96,7 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
// Use the custom hook
const { mostPopularForm } = useDefaultRoutingForm({
userId: userIdParsed,
teamId: teamIdParsed,
isAll: safe.success ? !!safe.data.teamId : false,
routingFormId: routingFormIdParsed,
@@ -29,6 +29,7 @@ const RoutingDownload = () => {
endDate: filter.dateRange[1].toISOString(),
teamId: filter.selectedTeamId ?? undefined,
userId: filter.selectedUserId ?? undefined,
memberUserId: filter.selectedMemberUserId ?? undefined,
routingFormId: filter.selectedRoutingFormId ?? undefined,
bookingStatus: filter.selectedBookingStatus ?? undefined,
fieldFilter: filter.selectedRoutingFormFilter ?? undefined,
@@ -34,6 +34,7 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
// Query routing form field options when showRoutingFilters is true
const { data: routingFormFieldOptions } = trpc.viewer.insights.getRoutingFormFieldOptions.useQuery(
{
userId: selectedUserId ?? -1,
teamId: selectedTeamId ?? -1,
isAll: !!isAll,
routingFormId: selectedRoutingFormId ?? undefined,
@@ -9,10 +9,11 @@ import { useFilterContext } from "../context/provider";
export const RoutingFormFieldFilter = ({ fieldId }: { fieldId: string }) => {
const { t } = useLocale();
const { filter, setConfigFilters } = useFilterContext();
const { selectedTeamId, isAll, selectedRoutingFormId, selectedRoutingFormFilter } = filter;
const { selectedTeamId, selectedUserId, isAll, selectedRoutingFormId, selectedRoutingFormFilter } = filter;
const { data: fieldOptions } = trpc.viewer.insights.getRoutingFormFieldOptions.useQuery(
{
userId: selectedUserId ?? undefined,
teamId: selectedTeamId ?? undefined,
isAll: !!isAll,
routingFormId: selectedRoutingFormId ?? undefined,
@@ -20,11 +20,12 @@ export const RoutingFormFilterList = memo(
({ showOnlyWhenSelectedInContext = true }: { showOnlyWhenSelectedInContext?: boolean }) => {
const { t } = useLocale();
const { filter, setConfigFilters } = useFilterContext();
const { selectedTeamId, selectedRoutingFormId, isAll } = filter;
const { selectedTeamId, selectedUserId, selectedRoutingFormId, isAll } = filter;
const { selectedFilter } = filter;
const { data: allForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery(
{
userId: selectedUserId ?? undefined,
teamId: selectedTeamId ?? undefined,
isAll: isAll ?? false,
},
@@ -100,6 +100,7 @@ export const TeamAndSelfList = ({
selectedTeamId: data[0].id,
selectedUserId: null,
selectedTeamName: null,
selectedRoutingFormId: null,
isAll: true,
});
}}
@@ -127,12 +128,14 @@ export const TeamAndSelfList = ({
selectedEventTypeId: null,
selectedMemberUserId: null,
selectedFilter: null,
selectedRoutingFormId: null,
});
} else if (!e.target.checked) {
setConfigFilters({
selectedTeamId: isOrgDataAvailable ? data[0].id : null,
selectedTeamName: null,
isAll: true,
selectedRoutingFormId: null,
isAll: isOrgDataAvailable,
});
}
}}
@@ -155,7 +158,9 @@ export const TeamAndSelfList = ({
onChange={(e) => {
if (e.target.checked) {
setConfigFilters({
selectedRoutingFormId: null,
selectedUserId: session.data?.user.id,
selectedMemberUserId: null,
selectedTeamId: null,
isAll: false,
});
@@ -164,6 +169,7 @@ export const TeamAndSelfList = ({
selectedTeamId: isOrgDataAvailable ? data[0].id : null,
selectedUserId: null,
selectedTeamName: null,
selectedRoutingFormId: null,
isAll: isOrgDataAvailable,
});
}
@@ -5,11 +5,13 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { trpc } from "@calcom/trpc";
export function useDefaultRoutingForm({
userId,
teamId,
isAll,
routingFormId,
onRoutingFormChange,
}: {
userId: number | null | undefined;
teamId: number | null | undefined;
isAll: boolean;
routingFormId: string | null | undefined;
@@ -24,6 +26,7 @@ export function useDefaultRoutingForm({
// Query to get routing forms list
const { data: routingForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery(
{
userId: userId ?? undefined,
teamId: teamId ?? undefined,
isAll,
},
@@ -11,6 +11,7 @@ import { readonlyPrisma as prisma } from "@calcom/prisma";
import type { BookingStatus } from "@calcom/prisma/enums";
type RoutingFormInsightsTeamFilter = {
userId?: number | null;
teamId?: number | null;
isAll: boolean;
organizationId?: number | null;
@@ -20,7 +21,7 @@ type RoutingFormInsightsTeamFilter = {
type RoutingFormInsightsFilter = RoutingFormInsightsTeamFilter & {
startDate?: string;
endDate?: string;
userId?: number | null;
memberUserId?: number | null;
searchQuery?: string | null;
bookingStatus?: BookingStatus | "NO_BOOKING" | null;
fieldFilter?: {
@@ -32,6 +33,7 @@ type RoutingFormInsightsFilter = RoutingFormInsightsTeamFilter & {
class RoutingEventsInsights {
private static async getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -55,23 +57,21 @@ class RoutingEventsInsights {
// Base where condition for forms
const formsWhereCondition: Prisma.App_RoutingForms_FormWhereInput = {
...(teamIds.length > 0 && {
teamId: {
in: teamIds,
},
}),
...(teamIds.length > 0
? {
teamId: {
in: teamIds,
},
}
: {
userId: userId ?? -1,
teamId: null,
}),
...(routingFormId && {
id: routingFormId,
}),
};
if (teamIds.length === 0 && !routingFormId) {
if (!organizationId) {
throw new Error("Organization ID is required");
}
formsWhereCondition.teamId = organizationId;
}
return formsWhereCondition;
}
@@ -83,12 +83,14 @@ class RoutingEventsInsights {
organizationId,
routingFormId,
userId,
memberUserId,
searchQuery,
bookingStatus,
fieldFilter,
}: Omit<RoutingFormInsightsFilter, "columnFilters">) {
// Get team IDs based on organization if applicable
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -104,13 +106,13 @@ class RoutingEventsInsights {
lte: dayjs(endDate).endOf("day").toDate(),
},
}),
...(userId || bookingStatus || searchQuery
...(memberUserId || bookingStatus || searchQuery
? {
...(bookingStatus === "NO_BOOKING"
? { routedToBooking: null }
: {
routedToBooking: {
...(userId && { userId }),
...(memberUserId && { userId: memberUserId }),
...(searchQuery && {
user: {
OR: [
@@ -166,16 +168,23 @@ class RoutingEventsInsights {
}
static async getRoutingFormsForFilters({
userId,
teamId,
isAll,
organizationId,
}: {
userId?: number;
teamId?: number;
isAll: boolean;
organizationId?: number | undefined;
routingFormId?: string | undefined;
}) {
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ teamId, isAll, organizationId });
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
});
return await prisma.app_RoutingForms_Form.findMany({
where: formsWhereCondition,
select: {
@@ -200,9 +209,11 @@ class RoutingEventsInsights {
cursor,
limit,
userId,
memberUserId,
columnFilters,
}: Omit<RoutingFormInsightsFilter, "fieldFilter" | "bookingStatus"> & { cursor?: number; limit?: number }) {
const formsTeamWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -218,8 +229,8 @@ class RoutingEventsInsights {
);
let bookingWhereInput: Prisma.BookingWhereInput = {};
if (userId) {
bookingWhereInput.userId = userId;
if (memberUserId) {
bookingWhereInput.userId = memberUserId;
}
if (bookingStatusFilter) {
bookingWhereInput = {
@@ -329,12 +340,14 @@ class RoutingEventsInsights {
}
static async getRoutingFormFieldOptions({
userId,
teamId,
isAll,
routingFormId,
organizationId,
}: RoutingFormInsightsTeamFilter) {
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
routingFormId,
@@ -355,12 +368,14 @@ class RoutingEventsInsights {
}
static async getFailedBookingsByRoutingFormGroup({
userId,
teamId,
isAll,
routingFormId,
organizationId,
}: RoutingFormInsightsTeamFilter) {
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -374,6 +389,10 @@ class RoutingEventsInsights {
// @ts-expect-error it doest exist but TS isnt smart enough when its unmber or int filter
teamConditions.push(`f."teamId" IN (${formsWhereCondition.teamId.in.join(",")})`);
}
// @ts-expect-error it doest exist but TS isnt smart enough when its unmber or int filter
if (!formsWhereCondition.teamId?.in && userId) {
teamConditions.push(`f.userId = '${userId}'`);
}
if (routingFormId) {
teamConditions.push(`f.id = '${routingFormId}'`);
}
@@ -486,12 +505,14 @@ class RoutingEventsInsights {
}
static async getRoutingFormHeaders({
userId,
teamId,
isAll,
organizationId,
routingFormId,
}: RoutingFormInsightsTeamFilter) {
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -528,12 +549,14 @@ class RoutingEventsInsights {
organizationId,
routingFormId,
userId,
memberUserId,
bookingStatus,
fieldFilter,
take,
skip,
}: Omit<RoutingFormInsightsFilter, "columnFilters"> & { take?: number; skip?: number }) {
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -587,13 +610,13 @@ class RoutingEventsInsights {
lte: dayjs(endDate).endOf("day").toDate(),
},
}),
...(userId || bookingStatus
...(memberUserId || bookingStatus
? {
...(bookingStatus === "NO_BOOKING"
? { routedToBooking: null }
: {
routedToBooking: {
...(userId && { userId }),
...(memberUserId && { userId: memberUserId }),
...(bookingStatus && { status: bookingStatus }),
},
}),
@@ -698,6 +721,7 @@ class RoutingEventsInsights {
}
static async routedToPerPeriod({
userId,
teamId,
isAll,
organizationId,
@@ -730,6 +754,7 @@ class RoutingEventsInsights {
// Build the team conditions for the WHERE clause
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -743,6 +768,10 @@ class RoutingEventsInsights {
// @ts-expect-error same as above
teamConditions.push(`f."teamId" IN (${formsWhereCondition.teamId.in.join(",")})`);
}
// @ts-expect-error it does exist but TS isn't smart enough when it's number or int filter
if (!formsWhereCondition.teamId?.in && userId) {
teamConditions.push(`f.userId = '${userId}'`);
}
if (routingFormId) {
teamConditions.push(`f.id = '${routingFormId}'`);
}
@@ -953,6 +982,7 @@ class RoutingEventsInsights {
}
static async routedToPerPeriodCsv({
userId,
teamId,
isAll,
organizationId,
@@ -979,6 +1009,7 @@ class RoutingEventsInsights {
// Build the team conditions for the WHERE clause
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
userId,
teamId,
isAll,
organizationId,
@@ -992,6 +1023,10 @@ class RoutingEventsInsights {
// @ts-expect-error same as above
teamConditions.push(`f."teamId" IN (${formsWhereCondition.teamId.in.join(",")})`);
}
// @ts-expect-error it does exist but TS isn't smart enough when it's number or int filter
if (!formsWhereCondition.teamId?.in && userId) {
teamConditions.push(`f.userId = '${userId}'`);
}
if (routingFormId) {
teamConditions.push(`f.id = '${routingFormId}'`);
}
@@ -1552,10 +1552,11 @@ export const insightsRouter = router({
}),
getRoutingFormsForFilters: userBelongsToTeamProcedure
.input(z.object({ teamId: z.number().optional(), isAll: z.boolean() }))
.input(z.object({ userId: z.number().optional(), teamId: z.number().optional(), isAll: z.boolean() }))
.query(async ({ ctx, input }) => {
const { teamId, isAll } = input;
const { userId, teamId, isAll } = input;
return await RoutingEventsInsights.getRoutingFormsForFilters({
userId,
teamId,
isAll,
organizationId: ctx.user.organizationId ?? undefined,
@@ -1585,6 +1586,7 @@ export const insightsRouter = router({
organizationId: ctx.user.organizationId ?? null,
routingFormId: input.routingFormId ?? null,
userId: input.userId ?? null,
memberUserId: input.memberUserId ?? null,
bookingStatus: input.bookingStatus ?? null,
fieldFilter: input.fieldFilter ?? null,
});
@@ -1611,13 +1613,19 @@ export const insightsRouter = router({
routingFormId: input.routingFormId ?? null,
cursor: input.cursor,
userId: input.userId ?? null,
memberUserId: input.memberUserId ?? null,
limit: input.limit,
columnFilters: input.columnFilters,
});
}),
getRoutingFormFieldOptions: userBelongsToTeamProcedure
.input(
z.object({ teamId: z.number().optional(), isAll: z.boolean(), routingFormId: z.string().optional() })
z.object({
userId: z.number().optional(),
teamId: z.number().optional(),
isAll: z.boolean(),
routingFormId: z.string().optional(),
})
)
.query(async ({ input, ctx }) => {
const options = await RoutingEventsInsights.getRoutingFormFieldOptions({
@@ -1628,7 +1636,12 @@ export const insightsRouter = router({
}),
failedBookingsByField: userBelongsToTeamProcedure
.input(
z.object({ teamId: z.number().optional(), isAll: z.boolean(), routingFormId: z.string().optional() })
z.object({
userId: z.number().optional(),
teamId: z.number().optional(),
isAll: z.boolean(),
routingFormId: z.string().optional(),
})
)
.query(async ({ ctx, input }) => {
return await RoutingEventsInsights.getFailedBookingsByRoutingFormGroup({
@@ -1639,6 +1652,7 @@ export const insightsRouter = router({
routingFormResponsesHeaders: userBelongsToTeamProcedure
.input(
z.object({
userId: z.number().optional(),
teamId: z.number().optional(),
isAll: z.boolean(),
routingFormId: z.string().optional(),
@@ -1646,6 +1660,7 @@ export const insightsRouter = router({
)
.query(async ({ ctx, input }) => {
const headers = await RoutingEventsInsights.getRoutingFormHeaders({
userId: input.userId ?? null,
teamId: input.teamId ?? null,
isAll: input.isAll,
organizationId: ctx.user.organizationId ?? null,
@@ -1669,8 +1684,18 @@ export const insightsRouter = router({
})
)
.query(async ({ ctx, input }) => {
const { teamId, startDate, endDate, userId, isAll, routingFormId, bookingStatus, fieldFilter, cursor } =
input;
const {
teamId,
startDate,
endDate,
userId,
memberUserId,
isAll,
routingFormId,
bookingStatus,
fieldFilter,
cursor,
} = input;
if (!teamId && !userId) {
return { data: [], hasMore: false, nextCursor: null };
@@ -1682,6 +1707,7 @@ export const insightsRouter = router({
startDate,
endDate,
userId,
memberUserId,
isAll: isAll ?? false,
organizationId: ctx.user.organizationId,
routingFormId,
@@ -1719,9 +1745,11 @@ export const insightsRouter = router({
})
)
.query(async ({ ctx, input }) => {
const { teamId, startDate, endDate, period, cursor, limit, isAll, routingFormId, searchQuery } = input;
const { teamId, userId, startDate, endDate, period, cursor, limit, isAll, routingFormId, searchQuery } =
input;
return await RoutingEventsInsights.routedToPerPeriod({
userId: userId ?? null,
teamId: teamId ?? null,
startDate,
endDate,
@@ -1747,6 +1775,7 @@ export const insightsRouter = router({
const { startDate, endDate } = input;
try {
const csvData = await RoutingEventsInsights.routedToPerPeriodCsv({
userId: input.userId ?? null,
teamId: input.teamId ?? null,
startDate,
endDate,