perf: lazy load tRPC routes (#8167)
* experiment: cold start perf * fix: update failing test * chore: add database indexes * chore: use json protocol and add query batching back * Update [status].tsx * Update [trpc].ts * Delete getSlimSession.ts * Update createContext.ts * remove trpc caller * correctly import Prisma * lazy ethRouter * replace crypto with md5 * import fixes * public event endpoint refactor * Update yarn.lock * Update yarn.lock * Using yarn.lock from main --------- Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Efraín Rochín <roae.85@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
parent
a4725920ff
commit
1eeb91a793
|
@ -6,6 +6,6 @@
|
||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"spellright.language": ["en"],
|
"spellright.language": ["en"],
|
||||||
"spellright.documentTypes": ["markdown", "typescript"],
|
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
|
||||||
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,5 +71,5 @@ module.exports = {
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
typescript: { reactDocgen: 'react-docgen' }
|
typescript: { reactDocgen: "react-docgen" },
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
|
@ -483,12 +483,12 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useLocale();
|
} = useLocale();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const recurringCount = recurringDates.filter((date) => {
|
const recurringCount = recurringDates.filter((recurringDate) => {
|
||||||
return (
|
return (
|
||||||
date >= now &&
|
recurringDate >= now &&
|
||||||
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||||
.map((date) => date.toDateString())
|
.map((date) => date.toDateString())
|
||||||
.includes(date.toDateString())
|
.includes(recurringDate.toDateString())
|
||||||
);
|
);
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
|
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
|
||||||
|
|
||||||
enum DirectAction {
|
enum DirectAction {
|
||||||
ACCEPT = "accept",
|
ACCEPT = "accept",
|
||||||
|
@ -51,7 +51,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||||
try {
|
try {
|
||||||
/** @see https://trpc.io/docs/server-side-calls */
|
/** @see https://trpc.io/docs/server-side-calls */
|
||||||
const ctx = await createContext({ req, res }, sessionGetter);
|
const ctx = await createContext({ req, res }, sessionGetter);
|
||||||
const caller = viewerRouter.createCaller(ctx);
|
const caller = viewerRouter.createCaller({ ...ctx, req, res });
|
||||||
|
|
||||||
await caller.bookings.confirm({
|
await caller.bookings.confirm({
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
recurringEventId: booking.recurringEventId || undefined,
|
recurringEventId: booking.recurringEventId || undefined,
|
||||||
|
|
|
@ -15,15 +15,14 @@ import { v4 as uuidv4 } from "uuid";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { BookingStatus } from "@calcom/prisma/client";
|
import type { BookingStatus } from "@calcom/prisma/client";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots";
|
import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||||
|
|
||||||
import { prismaMock, CalendarManagerMock } from "../../../../tests/config/singleton";
|
import { prismaMock, CalendarManagerMock } from "../../../../tests/config/singleton";
|
||||||
|
|
||||||
// TODO: Mock properly
|
// TODO: Mock properly
|
||||||
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
||||||
prismaMock.user.findMany.mockResolvedValue([]);
|
prismaMock.user.findMany.mockResolvedValue([]);
|
||||||
prismaMock.selectedSlots.findMany.mockResolvedValue([]);
|
|
||||||
|
|
||||||
jest.mock("@calcom/lib/constants", () => ({
|
jest.mock("@calcom/lib/constants", () => ({
|
||||||
IS_PRODUCTION: true,
|
IS_PRODUCTION: true,
|
||||||
|
@ -271,16 +270,13 @@ describe("getSchedule", () => {
|
||||||
end: `${plus2DateString}T23:00:00.000Z`,
|
end: `${plus2DateString}T23:00:00.000Z`,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule(
|
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||||
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
|
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
|
||||||
|
@ -357,17 +353,14 @@ describe("getSchedule", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Day Plus 2 is completely free - It only has non accepted bookings
|
// Day Plus 2 is completely free - It only has non accepted bookings
|
||||||
const scheduleOnCompletelyFreeDay = await getSchedule(
|
const scheduleOnCompletelyFreeDay = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
// EventTypeSlug doesn't matter for non-dynamic events
|
// EventTypeSlug doesn't matter for non-dynamic events
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
// getSchedule returns timeslots in GMT
|
// getSchedule returns timeslots in GMT
|
||||||
expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots(
|
expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots(
|
||||||
|
@ -390,16 +383,13 @@ describe("getSchedule", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Day plus 3
|
// Day plus 3
|
||||||
const scheduleForDayWithOneBooking = await getSchedule(
|
const scheduleForDayWithOneBooking = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
|
@ -455,16 +445,13 @@ describe("getSchedule", () => {
|
||||||
});
|
});
|
||||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||||
const scheduleForEventWith30Length = await getSchedule(
|
const scheduleForEventWith30Length = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
`04:00:00.000Z`,
|
`04:00:00.000Z`,
|
||||||
|
@ -490,16 +477,13 @@ describe("getSchedule", () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule(
|
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
// `slotInterval` takes precedence over `length`
|
// `slotInterval` takes precedence over `length`
|
||||||
expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots(
|
expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots(
|
||||||
[`04:00:00.000Z`, `06:00:00.000Z`, `08:00:00.000Z`, `10:00:00.000Z`, `12:00:00.000Z`],
|
[`04:00:00.000Z`, `06:00:00.000Z`, `08:00:00.000Z`, `10:00:00.000Z`, `12:00:00.000Z`],
|
||||||
|
@ -553,16 +537,13 @@ describe("getSchedule", () => {
|
||||||
});
|
});
|
||||||
const { dateString: todayDateString } = getDate();
|
const { dateString: todayDateString } = getDate();
|
||||||
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
||||||
const scheduleForEventWithBookingNotice13Hrs = await getSchedule(
|
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
/*`04:00:00.000Z`, `06:00:00.000Z`, - Minimum time slot is 07:30 UTC*/ `08:00:00.000Z`,
|
/*`04:00:00.000Z`, `06:00:00.000Z`, - Minimum time slot is 07:30 UTC*/ `08:00:00.000Z`,
|
||||||
|
@ -574,16 +555,13 @@ describe("getSchedule", () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduleForEventWithBookingNotice10Hrs = await getSchedule(
|
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
/*`04:00:00.000Z`, - Minimum bookable time slot is 04:30 UTC but next available is 06:00*/
|
/*`04:00:00.000Z`, - Minimum bookable time slot is 04:30 UTC but next available is 06:00*/
|
||||||
|
@ -639,16 +617,13 @@ describe("getSchedule", () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const scheduleForEventOnADayWithNonCalBooking = await getSchedule(
|
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
|
@ -714,16 +689,13 @@ describe("getSchedule", () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const scheduleForEventOnADayWithCalBooking = await getSchedule(
|
const scheduleForEventOnADayWithCalBooking = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
|
@ -767,16 +739,13 @@ describe("getSchedule", () => {
|
||||||
|
|
||||||
createBookingScenario(scenarioData);
|
createBookingScenario(scenarioData);
|
||||||
|
|
||||||
const scheduleForEventOnADayWithDateOverride = await getSchedule(
|
const scheduleForEventOnADayWithDateOverride = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
||||||
["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"],
|
["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"],
|
||||||
|
@ -853,16 +822,13 @@ describe("getSchedule", () => {
|
||||||
|
|
||||||
// Requesting this user's availability for their
|
// Requesting this user's availability for their
|
||||||
// individual Event Type
|
// individual Event Type
|
||||||
const thisUserAvailability = await getSchedule(
|
const thisUserAvailability = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(thisUserAvailability).toHaveTimeSlots(
|
expect(thisUserAvailability).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
|
@ -951,16 +917,13 @@ describe("getSchedule", () => {
|
||||||
hosts: [],
|
hosts: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
|
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
|
@ -981,16 +944,13 @@ describe("getSchedule", () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule(
|
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
// A user with blocked time in another event, still affects Team Event availability
|
// A user with blocked time in another event, still affects Team Event availability
|
||||||
// It's a collective availability, so both user 101 and 102 are considered for timeslots
|
// It's a collective availability, so both user 101 and 102 are considered for timeslots
|
||||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUser).toHaveTimeSlots(
|
expect(scheduleForTeamEventOnADayWithOneBookingForEachUser).toHaveTimeSlots(
|
||||||
|
@ -1088,16 +1048,13 @@ describe("getSchedule", () => {
|
||||||
],
|
],
|
||||||
hosts: [],
|
hosts: [],
|
||||||
});
|
});
|
||||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule(
|
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
// A user with blocked time in another event, still affects Team Event availability
|
// A user with blocked time in another event, still affects Team Event availability
|
||||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
|
@ -1116,16 +1073,13 @@ describe("getSchedule", () => {
|
||||||
{ dateString: plus2DateString }
|
{ dateString: plus2DateString }
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule(
|
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
|
||||||
{
|
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
eventTypeSlug: "",
|
||||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
timeZone: Timezones["+5:30"],
|
||||||
},
|
});
|
||||||
ctx
|
|
||||||
);
|
|
||||||
// A user with blocked time in another event, still affects Team Event availability
|
// A user with blocked time in another event, still affects Team Event availability
|
||||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
|
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { checkBalance } from "../utils/ethereum";
|
||||||
|
import type { TBalanceInputSchema } from "./balance.schema";
|
||||||
|
|
||||||
|
interface BalanceHandlerOptions {
|
||||||
|
input: TBalanceInputSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const balanceHandler = async ({ input }: BalanceHandlerOptions) => {
|
||||||
|
const { address, tokenAddress, chainId } = input;
|
||||||
|
try {
|
||||||
|
const hasBalance = await checkBalance(address, tokenAddress, chainId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
hasBalance,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
hasBalance: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export const ZBalanceInputSchema = z.object({
|
||||||
|
address: z.string(),
|
||||||
|
tokenAddress: z.string(),
|
||||||
|
chainId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZBalanceOutputSchema = z.object({
|
||||||
|
data: z
|
||||||
|
.object({
|
||||||
|
hasBalance: z.boolean(),
|
||||||
|
})
|
||||||
|
.nullish(),
|
||||||
|
error: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TBalanceOutputSchema = z.infer<typeof ZBalanceOutputSchema>;
|
||||||
|
export type TBalanceInputSchema = z.infer<typeof ZBalanceInputSchema>;
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { configureChains, createClient } from "wagmi";
|
||||||
|
|
||||||
|
import abi from "../utils/abi.json";
|
||||||
|
import { getProviders, SUPPORTED_CHAINS } from "../utils/ethereum";
|
||||||
|
import type { TContractInputSchema } from "./contract.schema";
|
||||||
|
|
||||||
|
interface ContractHandlerOptions {
|
||||||
|
input: TContractInputSchema;
|
||||||
|
}
|
||||||
|
export const contractHandler = async ({ input }: ContractHandlerOptions) => {
|
||||||
|
const { address, chainId } = input;
|
||||||
|
const { provider } = configureChains(
|
||||||
|
SUPPORTED_CHAINS.filter((chain) => chain.id === chainId),
|
||||||
|
getProviders()
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
provider,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contract = new ethers.Contract(address, abi, client.provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = await contract.name();
|
||||||
|
const symbol = await contract.symbol();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
symbol: `$${symbol}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
name: address,
|
||||||
|
symbol: "$UNKNOWN",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export const ZContractInputSchema = z.object({
|
||||||
|
address: z.string(),
|
||||||
|
chainId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZContractOutputSchema = z.object({
|
||||||
|
data: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
symbol: z.string(),
|
||||||
|
})
|
||||||
|
.nullish(),
|
||||||
|
error: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TContractInputSchema = z.infer<typeof ZContractInputSchema>;
|
||||||
|
export type TContractOutputSchema = z.infer<typeof ZContractOutputSchema>;
|
|
@ -1,100 +1,53 @@
|
||||||
import { ethers } from "ethers";
|
|
||||||
import { configureChains, createClient } from "wagmi";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { router, publicProcedure } from "@calcom/trpc/server/trpc";
|
import { router, publicProcedure } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import abi from "../utils/abi.json";
|
import { ZBalanceInputSchema, ZBalanceOutputSchema } from "./balance.schema";
|
||||||
import { checkBalance, getProviders, SUPPORTED_CHAINS } from "../utils/ethereum";
|
import { ZContractInputSchema, ZContractOutputSchema } from "./contract.schema";
|
||||||
|
|
||||||
|
interface EthRouterHandlersCache {
|
||||||
|
contract?: typeof import("./contract.handler").contractHandler;
|
||||||
|
balance?: typeof import("./balance.handler").balanceHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UNSTABLE_HANDLER_CACHE: EthRouterHandlersCache = {};
|
||||||
|
|
||||||
const ethRouter = router({
|
const ethRouter = router({
|
||||||
// Fetch contract `name` and `symbol` or error
|
// Fetch contract `name` and `symbol` or error
|
||||||
contract: publicProcedure
|
contract: publicProcedure
|
||||||
.input(
|
.input(ZContractInputSchema)
|
||||||
z.object({
|
.output(ZContractOutputSchema)
|
||||||
address: z.string(),
|
|
||||||
chainId: z.number(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.output(
|
|
||||||
z.object({
|
|
||||||
data: z
|
|
||||||
.object({
|
|
||||||
name: z.string(),
|
|
||||||
symbol: z.string(),
|
|
||||||
})
|
|
||||||
.nullish(),
|
|
||||||
error: z.string().nullish(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { address, chainId } = input;
|
if (!UNSTABLE_HANDLER_CACHE.contract) {
|
||||||
const { provider } = configureChains(
|
UNSTABLE_HANDLER_CACHE.contract = await import("./contract.handler").then(
|
||||||
SUPPORTED_CHAINS.filter((chain) => chain.id === chainId),
|
(mod) => mod.contractHandler
|
||||||
getProviders()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = createClient({
|
|
||||||
provider,
|
|
||||||
});
|
|
||||||
|
|
||||||
const contract = new ethers.Contract(address, abi, client.provider);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const name = await contract.name();
|
|
||||||
const symbol = await contract.symbol();
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
symbol: `$${symbol}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
name: address,
|
|
||||||
symbol: "$UNKNOWN",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.contract) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.contract({
|
||||||
|
input,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
// Fetch user's `balance` of either ERC-20 or ERC-721 compliant token or error
|
// Fetch user's `balance` of either ERC-20 or ERC-721 compliant token or error
|
||||||
balance: publicProcedure
|
balance: publicProcedure
|
||||||
.input(
|
.input(ZBalanceInputSchema)
|
||||||
z.object({
|
.output(ZBalanceOutputSchema)
|
||||||
address: z.string(),
|
|
||||||
tokenAddress: z.string(),
|
|
||||||
chainId: z.number(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.output(
|
|
||||||
z.object({
|
|
||||||
data: z
|
|
||||||
.object({
|
|
||||||
hasBalance: z.boolean(),
|
|
||||||
})
|
|
||||||
.nullish(),
|
|
||||||
error: z.string().nullish(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { address, tokenAddress, chainId } = input;
|
if (!UNSTABLE_HANDLER_CACHE.balance) {
|
||||||
try {
|
UNSTABLE_HANDLER_CACHE.balance = await import("./balance.handler").then((mod) => mod.balanceHandler);
|
||||||
const hasBalance = await checkBalance(address, tokenAddress, chainId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
hasBalance,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
hasBalance: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.balance) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.balance({
|
||||||
|
input,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
|
||||||
export const mockEvent: RouterOutputs["viewer"]["public"]["event"] = {
|
export const mockEvent: RouterOutputs["viewer"]["public"]["event"] = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { HorizontalTabs } from "@calcom/ui";
|
import { HorizontalTabs } from "@calcom/ui";
|
||||||
import { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
|
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
|
||||||
|
|
||||||
import { FiltersContainer } from "../components/FiltersContainer";
|
import { FiltersContainer } from "../components/FiltersContainer";
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
import { prisma, bookingMinimalSelect } from "@calcom/prisma";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import crypto from "crypto";
|
import md5 from "md5";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
@ -715,7 +715,7 @@ export const insightsRouter = router({
|
||||||
return {
|
return {
|
||||||
userId: booking.userId,
|
userId: booking.userId,
|
||||||
user: userHashMap.get(booking.userId),
|
user: userHashMap.get(booking.userId),
|
||||||
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
|
emailMd5: md5(user?.email),
|
||||||
count: booking._count.id,
|
count: booking._count.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -806,7 +806,7 @@ export const insightsRouter = router({
|
||||||
return {
|
return {
|
||||||
userId: booking.userId,
|
userId: booking.userId,
|
||||||
user: userHashMap.get(booking.userId),
|
user: userHashMap.get(booking.userId),
|
||||||
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
|
emailMd5: md5(user?.email),
|
||||||
count: booking._count.id,
|
count: booking._count.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
|
||||||
export type Slots = RouterOutputs["viewer"]["public"]["slots"]["getSchedule"]["slots"];
|
export type Slots = RouterOutputs["viewer"]["public"]["slots"]["getSchedule"]["slots"];
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { trpc } from "@calcom/trpc/react";
|
||||||
import { Meta, showToast, SkeletonContainer } from "@calcom/ui";
|
import { Meta, showToast, SkeletonContainer } from "@calcom/ui";
|
||||||
|
|
||||||
import { getLayout } from "../../settings/layouts/SettingsLayout";
|
import { getLayout } from "../../settings/layouts/SettingsLayout";
|
||||||
import WebhookForm, { WebhookFormSubmitData } from "../components/WebhookForm";
|
import type { WebhookFormSubmitData } from "../components/WebhookForm";
|
||||||
|
import WebhookForm from "../components/WebhookForm";
|
||||||
|
|
||||||
const querySchema = z.object({ id: z.string() });
|
const querySchema = z.object({ id: z.string() });
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const logP = (message: string) => {
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const end = performance.now();
|
||||||
|
console.log(`[PERF]: ${message} took ${end - start}ms`);
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
import type { NextPageContext } from "next/types";
|
import type { NextPageContext } from "next/types";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
import { httpBatchLink } from "../client/links/httpBatchLink";
|
||||||
import { httpLink } from "../client/links/httpLink";
|
import { httpLink } from "../client/links/httpLink";
|
||||||
import { loggerLink } from "../client/links/loggerLink";
|
import { loggerLink } from "../client/links/loggerLink";
|
||||||
import { splitLink } from "../client/links/splitLink";
|
import { splitLink } from "../client/links/splitLink";
|
||||||
|
@ -8,7 +9,6 @@ import { createTRPCNext } from "../next";
|
||||||
// ℹ️ Type-only import:
|
// ℹ️ Type-only import:
|
||||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
||||||
import type { TRPCClientErrorLike } from "../react";
|
import type { TRPCClientErrorLike } from "../react";
|
||||||
import { httpBatchLink } from "../react";
|
|
||||||
import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server";
|
import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server";
|
||||||
import type { AppRouter } from "../server/routers/_app";
|
import type { AppRouter } from "../server/routers/_app";
|
||||||
|
|
||||||
|
|
|
@ -77,3 +77,5 @@ export const createContext = async (
|
||||||
res,
|
res,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TRPCContext = Awaited<ReturnType<typeof createContext>>;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* This file contains the root router of your tRPC-backend
|
* This file contains the root router of your tRPC-backend
|
||||||
*/
|
*/
|
||||||
import { router } from "../trpc";
|
import { router } from "../trpc";
|
||||||
import { viewerRouter } from "./viewer";
|
import { viewerRouter } from "./viewer/_router";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create your application's root router
|
* Create your application's root router
|
||||||
|
|
|
@ -0,0 +1,369 @@
|
||||||
|
import { authedProcedure, router } from "../../trpc";
|
||||||
|
import { ZAppByIdInputSchema } from "./appById.schema";
|
||||||
|
import { ZAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
|
||||||
|
import { ZAppsInputSchema } from "./apps.schema";
|
||||||
|
import { ZAwayInputSchema } from "./away.schema";
|
||||||
|
import { ZDeleteCredentialInputSchema } from "./deleteCredential.schema";
|
||||||
|
import { ZDeleteMeInputSchema } from "./deleteMe.schema";
|
||||||
|
import { ZEventTypeOrderInputSchema } from "./eventTypeOrder.schema";
|
||||||
|
import { ZGetCalVideoRecordingsInputSchema } from "./getCalVideoRecordings.schema";
|
||||||
|
import { ZGetDownloadLinkOfCalVideoRecordingsInputSchema } from "./getDownloadLinkOfCalVideoRecordings.schema";
|
||||||
|
import { ZIntegrationsInputSchema } from "./integrations.schema";
|
||||||
|
import { ZSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
|
||||||
|
import { ZSubmitFeedbackInputSchema } from "./submitFeedback.schema";
|
||||||
|
import { ZUpdateProfileInputSchema } from "./updateProfile.schema";
|
||||||
|
import { ZUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
|
||||||
|
|
||||||
|
type AppsRouterHandlerCache = {
|
||||||
|
me?: typeof import("./me.handler").meHandler;
|
||||||
|
avatar?: typeof import("./avatar.handler").avatarHandler;
|
||||||
|
deleteMe?: typeof import("./deleteMe.handler").deleteMeHandler;
|
||||||
|
deleteMeWithoutPassword?: typeof import("./deleteMeWithoutPassword.handler").deleteMeWithoutPasswordHandler;
|
||||||
|
away?: typeof import("./away.handler").awayHandler;
|
||||||
|
connectedCalendars?: typeof import("./connectedCalendars.handler").connectedCalendarsHandler;
|
||||||
|
setDestinationCalendar?: typeof import("./setDestinationCalendar.handler").setDestinationCalendarHandler;
|
||||||
|
integrations?: typeof import("./integrations.handler").integrationsHandler;
|
||||||
|
appById?: typeof import("./appById.handler").appByIdHandler;
|
||||||
|
apps?: typeof import("./apps.handler").appsHandler;
|
||||||
|
appCredentialsByType?: typeof import("./appCredentialsByType.handler").appCredentialsByTypeHandler;
|
||||||
|
stripeCustomer?: typeof import("./stripeCustomer.handler").stripeCustomerHandler;
|
||||||
|
updateProfile?: typeof import("./updateProfile.handler").updateProfileHandler;
|
||||||
|
eventTypeOrder?: typeof import("./eventTypeOrder.handler").eventTypeOrderHandler;
|
||||||
|
submitFeedback?: typeof import("./submitFeedback.handler").submitFeedbackHandler;
|
||||||
|
locationOptions?: typeof import("./locationOptions.handler").locationOptionsHandler;
|
||||||
|
deleteCredential?: typeof import("./deleteCredential.handler").deleteCredentialHandler;
|
||||||
|
bookingUnconfirmedCount?: typeof import("./bookingUnconfirmedCount.handler").bookingUnconfirmedCountHandler;
|
||||||
|
getCalVideoRecordings?: typeof import("./getCalVideoRecordings.handler").getCalVideoRecordingsHandler;
|
||||||
|
getDownloadLinkOfCalVideoRecordings?: typeof import("./getDownloadLinkOfCalVideoRecordings.handler").getDownloadLinkOfCalVideoRecordingsHandler;
|
||||||
|
getUsersDefaultConferencingApp?: typeof import("./getUsersDefaultConferencingApp.handler").getUsersDefaultConferencingAppHandler;
|
||||||
|
updateUserDefaultConferencingApp?: typeof import("./updateUserDefaultConferencingApp.handler").updateUserDefaultConferencingAppHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNSTABLE_HANDLER_CACHE: AppsRouterHandlerCache = {};
|
||||||
|
|
||||||
|
export const loggedInViewerRouter = router({
|
||||||
|
me: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.me) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.me = (await import("./me.handler")).meHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.me) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.me({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
avatar: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.avatar) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.avatar = (await import("./avatar.handler")).avatarHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.avatar) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.avatar({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteMe: authedProcedure.input(ZDeleteMeInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.deleteMe) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.deleteMe = (await import("./deleteMe.handler")).deleteMeHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.deleteMe) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.deleteMe({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteMeWithoutPassword: authedProcedure.mutation(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword = (
|
||||||
|
await import("./deleteMeWithoutPassword.handler")
|
||||||
|
).deleteMeWithoutPasswordHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.deleteMeWithoutPassword({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
away: authedProcedure.input(ZAwayInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.away) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.away = (await import("./away.handler")).awayHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.away) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.away({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
connectedCalendars: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.connectedCalendars) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.connectedCalendars = (
|
||||||
|
await import("./connectedCalendars.handler")
|
||||||
|
).connectedCalendarsHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.connectedCalendars) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.connectedCalendars({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
setDestinationCalendar: authedProcedure
|
||||||
|
.input(ZSetDestinationCalendarInputSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.setDestinationCalendar) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.setDestinationCalendar = (
|
||||||
|
await import("./setDestinationCalendar.handler")
|
||||||
|
).setDestinationCalendarHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.setDestinationCalendar) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.setDestinationCalendar({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
integrations: authedProcedure.input(ZIntegrationsInputSchema).query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.integrations) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.integrations = (await import("./integrations.handler")).integrationsHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.integrations) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.integrations({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
appById: authedProcedure.input(ZAppByIdInputSchema).query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.appById) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.appById = (await import("./appById.handler")).appByIdHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.appById) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.appById({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
apps: authedProcedure.input(ZAppsInputSchema).query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.apps) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.apps = (await import("./apps.handler")).appsHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.apps) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.apps({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
appCredentialsByType: authedProcedure
|
||||||
|
.input(ZAppCredentialsByTypeInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.appCredentialsByType) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.appCredentialsByType = (
|
||||||
|
await import("./appCredentialsByType.handler")
|
||||||
|
).appCredentialsByTypeHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.appCredentialsByType) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.appCredentialsByType({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
stripeCustomer: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.stripeCustomer) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.stripeCustomer = (
|
||||||
|
await import("./stripeCustomer.handler")
|
||||||
|
).stripeCustomerHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.stripeCustomer) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.stripeCustomer({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateProfile: authedProcedure.input(ZUpdateProfileInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.updateProfile) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.updateProfile = (await import("./updateProfile.handler")).updateProfileHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.updateProfile) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.updateProfile({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
eventTypeOrder: authedProcedure.input(ZEventTypeOrderInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.eventTypeOrder) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.eventTypeOrder = (
|
||||||
|
await import("./eventTypeOrder.handler")
|
||||||
|
).eventTypeOrderHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.eventTypeOrder) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.eventTypeOrder({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
//Comment for PR: eventTypePosition is not used anywhere
|
||||||
|
submitFeedback: authedProcedure.input(ZSubmitFeedbackInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.submitFeedback) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.submitFeedback = (
|
||||||
|
await import("./submitFeedback.handler")
|
||||||
|
).submitFeedbackHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.submitFeedback) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.submitFeedback({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
locationOptions: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.locationOptions) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.locationOptions = (
|
||||||
|
await import("./locationOptions.handler")
|
||||||
|
).locationOptionsHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.locationOptions) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.locationOptions({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteCredential: authedProcedure.input(ZDeleteCredentialInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.deleteCredential) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.deleteCredential = (
|
||||||
|
await import("./deleteCredential.handler")
|
||||||
|
).deleteCredentialHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.deleteCredential) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.deleteCredential({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
bookingUnconfirmedCount: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount = (
|
||||||
|
await import("./bookingUnconfirmedCount.handler")
|
||||||
|
).bookingUnconfirmedCountHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.bookingUnconfirmedCount({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
getCalVideoRecordings: authedProcedure
|
||||||
|
.input(ZGetCalVideoRecordingsInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.getCalVideoRecordings) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.getCalVideoRecordings = (
|
||||||
|
await import("./getCalVideoRecordings.handler")
|
||||||
|
).getCalVideoRecordingsHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.getCalVideoRecordings) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.getCalVideoRecordings({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
getDownloadLinkOfCalVideoRecordings: authedProcedure
|
||||||
|
.input(ZGetDownloadLinkOfCalVideoRecordingsInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings = (
|
||||||
|
await import("./getDownloadLinkOfCalVideoRecordings.handler")
|
||||||
|
).getDownloadLinkOfCalVideoRecordingsHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.getDownloadLinkOfCalVideoRecordings({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
getUsersDefaultConferencingApp: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp = (
|
||||||
|
await import("./getUsersDefaultConferencingApp.handler")
|
||||||
|
).getUsersDefaultConferencingAppHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.getUsersDefaultConferencingApp({ ctx });
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateUserDefaultConferencingApp: authedProcedure
|
||||||
|
.input(ZUpdateUserDefaultConferencingAppInputSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp = (
|
||||||
|
await import("./updateUserDefaultConferencingApp.handler")
|
||||||
|
).updateUserDefaultConferencingAppHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.updateUserDefaultConferencingApp({ ctx, input });
|
||||||
|
}),
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
import getApps from "@calcom/app-store/utils";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TAppByIdInputSchema } from "./appById.schema";
|
||||||
|
|
||||||
|
type AppByIdOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TAppByIdInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appByIdHandler = async ({ ctx, input }: AppByIdOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
const appId = input.appId;
|
||||||
|
const { credentials } = user;
|
||||||
|
const apps = getApps(credentials);
|
||||||
|
const appFromDb = apps.find((app) => app.slug === appId);
|
||||||
|
if (!appFromDb) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find app ${appId}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { credential: _, credentials: _1, ...app } = appFromDb;
|
||||||
|
return {
|
||||||
|
isInstalled: appFromDb.credentials.length,
|
||||||
|
...app,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZAppByIdInputSchema = z.object({
|
||||||
|
appId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAppByIdInputSchema = z.infer<typeof ZAppByIdInputSchema>;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import type { TAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
|
||||||
|
|
||||||
|
type AppCredentialsByTypeOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TAppCredentialsByTypeInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appCredentialsByTypeHandler = async ({ ctx, input }: AppCredentialsByTypeOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
return user.credentials.filter((app) => app.type == input.appType).map((credential) => credential.id);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZAppCredentialsByTypeInputSchema = z.object({
|
||||||
|
appType: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAppCredentialsByTypeInputSchema = z.infer<typeof ZAppCredentialsByTypeInputSchema>;
|
|
@ -0,0 +1,24 @@
|
||||||
|
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import type { TAppsInputSchema } from "./apps.schema";
|
||||||
|
|
||||||
|
type AppsOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TAppsInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appsHandler = async ({ ctx, input }: AppsOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
const { credentials } = user;
|
||||||
|
|
||||||
|
const apps = await getEnabledApps(credentials);
|
||||||
|
return apps
|
||||||
|
.filter((app) => app.extendsFeature?.includes(input.extendsFeature))
|
||||||
|
.map((app) => ({
|
||||||
|
...app,
|
||||||
|
isInstalled: !!app.credentials?.length,
|
||||||
|
}));
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZAppsInputSchema = z.object({
|
||||||
|
extendsFeature: z.literal("EventType"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAppsInputSchema = z.infer<typeof ZAppsInputSchema>;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
type AvatarOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const avatarHandler = async ({ ctx }: AvatarOptions) => {
|
||||||
|
return {
|
||||||
|
avatar: ctx.user.rawAvatar,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import type { TAwayInputSchema } from "./away.schema";
|
||||||
|
|
||||||
|
type AwayOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TAwayInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const awayHandler = async ({ ctx, input }: AwayOptions) => {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
email: ctx.user.email,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
away: input.away,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZAwayInputSchema = z.object({
|
||||||
|
away: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAwayInputSchema = z.infer<typeof ZAwayInputSchema>;
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { BookingStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
type BookingUnconfirmedCountOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bookingUnconfirmedCountHandler = async ({ ctx }: BookingUnconfirmedCountOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
const count = await prisma.booking.count({
|
||||||
|
where: {
|
||||||
|
status: BookingStatus.PENDING,
|
||||||
|
userId: user.id,
|
||||||
|
endTime: { gt: new Date() },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const recurringGrouping = await prisma.booking.groupBy({
|
||||||
|
by: ["recurringEventId"],
|
||||||
|
_count: {
|
||||||
|
recurringEventId: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
recurringEventId: { not: { equals: null } },
|
||||||
|
status: { equals: "PENDING" },
|
||||||
|
userId: user.id,
|
||||||
|
endTime: { gt: new Date() },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return recurringGrouping.reduce((prev, current) => {
|
||||||
|
// recurringEventId is the total number of recurring instances for a booking
|
||||||
|
// we need to subtract all but one, to represent a single recurring booking
|
||||||
|
return prev - (current._count?.recurringEventId - 1);
|
||||||
|
}, count);
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,90 @@
|
||||||
|
import type { DestinationCalendar } from "@prisma/client";
|
||||||
|
import { AppCategories } from "@prisma/client";
|
||||||
|
|
||||||
|
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
type ConnectedCalendarsOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectedCalendarsHandler = async ({ ctx }: ConnectedCalendarsOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
|
||||||
|
const userCredentials = await prisma.credential.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
app: {
|
||||||
|
categories: { has: AppCategories.calendar },
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// get user's credentials + their connected integrations
|
||||||
|
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||||
|
|
||||||
|
// get all the connected integrations' calendars (from third party)
|
||||||
|
const { connectedCalendars, destinationCalendar } = await getConnectedCalendars(
|
||||||
|
calendarCredentials,
|
||||||
|
user.selectedCalendars,
|
||||||
|
user.destinationCalendar?.externalId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (connectedCalendars.length === 0) {
|
||||||
|
/* As there are no connected calendars, delete the destination calendar if it exists */
|
||||||
|
if (user.destinationCalendar) {
|
||||||
|
await prisma.destinationCalendar.delete({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
user.destinationCalendar = null;
|
||||||
|
}
|
||||||
|
} else if (!user.destinationCalendar) {
|
||||||
|
/*
|
||||||
|
There are connected calendars, but no destination calendar
|
||||||
|
So create a default destination calendar with the first primary connected calendar
|
||||||
|
*/
|
||||||
|
const { integration = "", externalId = "", credentialId } = connectedCalendars[0].primary ?? {};
|
||||||
|
user.destinationCalendar = await prisma.destinationCalendar.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
integration,
|
||||||
|
externalId,
|
||||||
|
credentialId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
/* There are connected calendars and a destination calendar */
|
||||||
|
|
||||||
|
// Check if destinationCalendar exists in connectedCalendars
|
||||||
|
const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat();
|
||||||
|
const destinationCal = allCals.find(
|
||||||
|
(cal) =>
|
||||||
|
cal.externalId === user.destinationCalendar?.externalId &&
|
||||||
|
cal.integration === user.destinationCalendar?.integration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!destinationCal) {
|
||||||
|
// If destinationCalendar is out of date, update it with the first primary connected calendar
|
||||||
|
const { integration = "", externalId = "" } = connectedCalendars[0].primary ?? {};
|
||||||
|
user.destinationCalendar = await prisma.destinationCalendar.update({
|
||||||
|
where: { userId: user.id },
|
||||||
|
data: {
|
||||||
|
integration,
|
||||||
|
externalId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectedCalendars,
|
||||||
|
destinationCalendar: {
|
||||||
|
...(user.destinationCalendar as DestinationCalendar),
|
||||||
|
...destinationCalendar,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,345 @@
|
||||||
|
import { AppCategories, BookingStatus } from "@prisma/client";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||||
|
import { DailyLocationType } from "@calcom/core/location";
|
||||||
|
import { sendCancelledEmails } from "@calcom/emails";
|
||||||
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
|
import { deletePayment } from "@calcom/lib/payment/deletePayment";
|
||||||
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
|
import { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TDeleteCredentialInputSchema } from "./deleteCredential.schema";
|
||||||
|
|
||||||
|
type DeleteCredentialOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TDeleteCredentialInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOptions) => {
|
||||||
|
const { id, externalId } = input;
|
||||||
|
|
||||||
|
const credential = await prisma.credential.findFirst({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
key: true,
|
||||||
|
appId: true,
|
||||||
|
app: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
categories: true,
|
||||||
|
dirName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!credential) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTypes = await prisma.eventType.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
locations: true,
|
||||||
|
destinationCalendar: {
|
||||||
|
include: {
|
||||||
|
credential: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
price: true,
|
||||||
|
currency: true,
|
||||||
|
metadata: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Improve this uninstallation cleanup per event by keeping a relation of EventType to App which has the data.
|
||||||
|
for (const eventType of eventTypes) {
|
||||||
|
if (eventType.locations) {
|
||||||
|
// If it's a video, replace the location with Cal video
|
||||||
|
if (credential.app?.categories.includes(AppCategories.video)) {
|
||||||
|
// Find the user's event types
|
||||||
|
|
||||||
|
// Look for integration name from app slug
|
||||||
|
const integrationQuery =
|
||||||
|
credential.app?.slug === "msteams" ? "office365_video" : credential.app?.slug.split("-")[0];
|
||||||
|
|
||||||
|
// Check if the event type uses the deleted integration
|
||||||
|
|
||||||
|
// To avoid type errors, need to stringify and parse JSON to use array methods
|
||||||
|
const locationsSchema = z.array(z.object({ type: z.string() }));
|
||||||
|
const locations = locationsSchema.parse(eventType.locations);
|
||||||
|
|
||||||
|
const updatedLocations = locations.map((location: { type: string }) => {
|
||||||
|
if (location.type.includes(integrationQuery)) {
|
||||||
|
return { type: DailyLocationType };
|
||||||
|
}
|
||||||
|
return location;
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.eventType.update({
|
||||||
|
where: {
|
||||||
|
id: eventType.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
locations: updatedLocations,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a calendar, remove the destination calendar from the event type
|
||||||
|
if (credential.app?.categories.includes(AppCategories.calendar)) {
|
||||||
|
if (eventType.destinationCalendar?.credential?.appId === credential.appId) {
|
||||||
|
const destinationCalendar = await prisma.destinationCalendar.findFirst({
|
||||||
|
where: {
|
||||||
|
id: eventType.destinationCalendar?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (destinationCalendar) {
|
||||||
|
await prisma.destinationCalendar.delete({
|
||||||
|
where: {
|
||||||
|
id: destinationCalendar.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (externalId) {
|
||||||
|
const existingSelectedCalendar = await prisma.selectedCalendar.findFirst({
|
||||||
|
where: {
|
||||||
|
externalId: externalId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// @TODO: SelectedCalendar doesn't have unique ID so we should only delete one item
|
||||||
|
if (existingSelectedCalendar) {
|
||||||
|
await prisma.selectedCalendar.delete({
|
||||||
|
where: {
|
||||||
|
userId_integration_externalId: {
|
||||||
|
userId: existingSelectedCalendar.userId,
|
||||||
|
externalId: existingSelectedCalendar.externalId,
|
||||||
|
integration: existingSelectedCalendar.integration,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
||||||
|
|
||||||
|
const stripeAppData = getPaymentAppData({ ...eventType, metadata });
|
||||||
|
|
||||||
|
// If it's a payment, hide the event type and set the price to 0. Also cancel all pending bookings
|
||||||
|
if (credential.app?.categories.includes(AppCategories.payment)) {
|
||||||
|
if (stripeAppData.price) {
|
||||||
|
await prisma.$transaction(async () => {
|
||||||
|
await prisma.eventType.update({
|
||||||
|
where: {
|
||||||
|
id: eventType.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
hidden: true,
|
||||||
|
metadata: {
|
||||||
|
...metadata,
|
||||||
|
apps: {
|
||||||
|
...metadata?.apps,
|
||||||
|
stripe: {
|
||||||
|
...metadata?.apps?.stripe,
|
||||||
|
price: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assuming that all bookings under this eventType need to be paid
|
||||||
|
const unpaidBookings = await prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
status: "PENDING",
|
||||||
|
paid: false,
|
||||||
|
payment: {
|
||||||
|
every: {
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...bookingMinimalSelect,
|
||||||
|
recurringEventId: true,
|
||||||
|
userId: true,
|
||||||
|
responses: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
credentials: true,
|
||||||
|
email: true,
|
||||||
|
timeZone: true,
|
||||||
|
name: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
locale: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: true,
|
||||||
|
references: {
|
||||||
|
select: {
|
||||||
|
uid: true,
|
||||||
|
type: true,
|
||||||
|
externalCalendarId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
payment: true,
|
||||||
|
paid: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
recurringEvent: true,
|
||||||
|
title: true,
|
||||||
|
bookingFields: true,
|
||||||
|
seatsPerTimeSlot: true,
|
||||||
|
seatsShowAttendees: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uid: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const booking of unpaidBookings) {
|
||||||
|
await prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: booking.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.CANCELLED,
|
||||||
|
cancellationReason: "Payment method removed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const payment of booking.payment) {
|
||||||
|
try {
|
||||||
|
await deletePayment(payment.id, credential);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
await prisma.payment.delete({
|
||||||
|
where: {
|
||||||
|
id: payment.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.attendee.deleteMany({
|
||||||
|
where: {
|
||||||
|
bookingId: booking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.bookingReference.deleteMany({
|
||||||
|
where: {
|
||||||
|
bookingId: booking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
timeZone: attendee.timeZone,
|
||||||
|
language: {
|
||||||
|
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||||
|
locale: attendee.locale ?? "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
const tOrganizer = await getTranslation(booking?.user?.locale ?? "en", "common");
|
||||||
|
await sendCancelledEmails({
|
||||||
|
type: booking?.eventType?.title as string,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description,
|
||||||
|
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||||
|
...getCalEventResponses({
|
||||||
|
bookingFields: booking.eventType?.bookingFields ?? null,
|
||||||
|
booking,
|
||||||
|
}),
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
organizer: {
|
||||||
|
email: booking?.user?.email as string,
|
||||||
|
name: booking?.user?.name ?? "Nameless",
|
||||||
|
timeZone: booking?.user?.timeZone as string,
|
||||||
|
language: { translate: tOrganizer, locale: booking?.user?.locale ?? "en" },
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
uid: booking.uid,
|
||||||
|
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||||
|
location: booking.location,
|
||||||
|
destinationCalendar: booking.destinationCalendar || booking.user?.destinationCalendar,
|
||||||
|
cancellationReason: "Payment method removed by organizer",
|
||||||
|
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||||
|
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if zapier get disconnected, delete zapier apiKey, delete zapier webhooks and cancel all scheduled jobs from zapier
|
||||||
|
if (credential.app?.slug === "zapier") {
|
||||||
|
await prisma.apiKey.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
appId: "zapier",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.webhook.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
appId: "zapier",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const bookingsWithScheduledJobs = await prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
scheduledJobs: {
|
||||||
|
isEmpty: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const booking of bookingsWithScheduledJobs) {
|
||||||
|
cancelScheduledJobs(booking, credential.appId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validated that credential is user's above
|
||||||
|
await prisma.credential.delete({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Revalidate user calendar cache.
|
||||||
|
if (credential.app?.slug.includes("calendar")) {
|
||||||
|
await fetch(`${WEBAPP_URL}/api/revalidate-calendar-cache/${ctx?.user?.username}`);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZDeleteCredentialInputSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
externalId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TDeleteCredentialInputSchema = z.infer<typeof ZDeleteCredentialInputSchema>;
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { IdentityProvider } from "@prisma/client";
|
||||||
|
import { authenticator } from "otplib";
|
||||||
|
|
||||||
|
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
|
||||||
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
|
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||||
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import type { TDeleteMeInputSchema } from "./deleteMe.schema";
|
||||||
|
|
||||||
|
type DeleteMeOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TDeleteMeInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMeHandler = async ({ ctx, input }: DeleteMeOptions) => {
|
||||||
|
// Check if input.password is correct
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email: ctx.user.email.toLowerCase(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(ErrorCode.UserNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.identityProvider !== IdentityProvider.CAL) {
|
||||||
|
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.password) {
|
||||||
|
throw new Error(ErrorCode.UserMissingPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCorrectPassword = await verifyPassword(input.password, user.password);
|
||||||
|
if (!isCorrectPassword) {
|
||||||
|
throw new Error(ErrorCode.IncorrectPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.twoFactorEnabled) {
|
||||||
|
if (!input.totpCode) {
|
||||||
|
throw new Error(ErrorCode.SecondFactorRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.twoFactorSecret) {
|
||||||
|
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
|
||||||
|
throw new Error(ErrorCode.InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||||
|
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
|
||||||
|
throw new Error(ErrorCode.InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
|
||||||
|
if (secret.length !== 32) {
|
||||||
|
console.error(
|
||||||
|
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
|
||||||
|
);
|
||||||
|
throw new Error(ErrorCode.InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has 2fa enabled, check if input.totpCode is correct
|
||||||
|
const isValidToken = authenticator.check(input.totpCode, secret);
|
||||||
|
if (!isValidToken) {
|
||||||
|
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If 2FA is disabled or totpCode is valid then delete the user from stripe and database
|
||||||
|
await deleteStripeCustomer(user).catch(console.warn);
|
||||||
|
// Remove my account
|
||||||
|
const deletedUser = await prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync Services
|
||||||
|
syncServicesDeleteWebUser(deletedUser);
|
||||||
|
return;
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZDeleteMeInputSchema = z.object({
|
||||||
|
password: z.string(),
|
||||||
|
totpCode: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TDeleteMeInputSchema = z.infer<typeof ZDeleteMeInputSchema>;
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { IdentityProvider } from "@prisma/client";
|
||||||
|
|
||||||
|
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
|
||||||
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
|
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
type DeleteMeWithoutPasswordOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMeWithoutPasswordHandler = async ({ ctx }: DeleteMeWithoutPasswordOptions) => {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email: ctx.user.email.toLowerCase(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(ErrorCode.UserNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.identityProvider === IdentityProvider.CAL) {
|
||||||
|
throw new Error(ErrorCode.SocialIdentityProviderRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.twoFactorEnabled) {
|
||||||
|
throw new Error(ErrorCode.SocialIdentityProviderRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove me from Stripe
|
||||||
|
await deleteStripeCustomer(user).catch(console.warn);
|
||||||
|
|
||||||
|
// Remove my account
|
||||||
|
const deletedUser = await prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Sync Services
|
||||||
|
syncServicesDeleteWebUser(deletedUser);
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { reverse } from "lodash";
|
||||||
|
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TEventTypeOrderInputSchema } from "./eventTypeOrder.schema";
|
||||||
|
|
||||||
|
type EventTypeOrderOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TEventTypeOrderInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const eventTypeOrderHandler = async ({ ctx, input }: EventTypeOrderOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
|
||||||
|
const allEventTypes = await prisma.eventType.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: input.ids,
|
||||||
|
},
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
users: {
|
||||||
|
some: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const allEventTypeIds = new Set(allEventTypes.map((type) => type.id));
|
||||||
|
if (input.ids.some((id) => !allEventTypeIds.has(id))) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
reverse(input.ids).map((id, position) => {
|
||||||
|
return prisma.eventType.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZEventTypeOrderInputSchema = z.object({
|
||||||
|
ids: z.array(z.number()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TEventTypeOrderInputSchema = z.infer<typeof ZEventTypeOrderInputSchema>;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { getRecordingsOfCalVideoByRoomName } from "@calcom/core/videoClient";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TGetCalVideoRecordingsInputSchema } from "./getCalVideoRecordings.schema";
|
||||||
|
|
||||||
|
type GetCalVideoRecordingsOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TGetCalVideoRecordingsInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCalVideoRecordingsHandler = async ({ ctx: _ctx, input }: GetCalVideoRecordingsOptions) => {
|
||||||
|
const { roomName } = input;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getRecordingsOfCalVideoByRoomName(roomName);
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZGetCalVideoRecordingsInputSchema = z.object({
|
||||||
|
roomName: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGetCalVideoRecordingsInputSchema = z.infer<typeof ZGetCalVideoRecordingsInputSchema>;
|
|
@ -0,0 +1,38 @@
|
||||||
|
/// <reference types="@calcom/types/next-auth" />
|
||||||
|
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||||
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { CreateInnerContextOptions } from "../../createContext";
|
||||||
|
import type { TGetDownloadLinkOfCalVideoRecordingsInputSchema } from "./getDownloadLinkOfCalVideoRecordings.schema";
|
||||||
|
|
||||||
|
type GetDownloadLinkOfCalVideoRecordingsHandlerOptions = {
|
||||||
|
ctx: CreateInnerContextOptions;
|
||||||
|
input: TGetDownloadLinkOfCalVideoRecordingsInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDownloadLinkOfCalVideoRecordingsHandler = async ({
|
||||||
|
input,
|
||||||
|
ctx,
|
||||||
|
}: GetDownloadLinkOfCalVideoRecordingsHandlerOptions) => {
|
||||||
|
const { recordingId } = input;
|
||||||
|
const { session } = ctx;
|
||||||
|
|
||||||
|
const isDownloadAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
|
||||||
|
|
||||||
|
if (!isDownloadAllowed) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZGetDownloadLinkOfCalVideoRecordingsInputSchema = z.object({
|
||||||
|
recordingId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGetDownloadLinkOfCalVideoRecordingsInputSchema = z.infer<
|
||||||
|
typeof ZGetDownloadLinkOfCalVideoRecordingsInputSchema
|
||||||
|
>;
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
type GetUsersDefaultConferencingAppOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsersDefaultConferencingAppHandler = async ({
|
||||||
|
ctx,
|
||||||
|
}: GetUsersDefaultConferencingAppOptions) => {
|
||||||
|
return userMetadata.parse(ctx.user.metadata)?.defaultConferencingApp;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,52 @@
|
||||||
|
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import type { TIntegrationsInputSchema } from "./integrations.schema";
|
||||||
|
|
||||||
|
type IntegrationsOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TIntegrationsInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
const { variant, exclude, onlyInstalled } = input;
|
||||||
|
const { credentials } = user;
|
||||||
|
|
||||||
|
const enabledApps = await getEnabledApps(credentials);
|
||||||
|
//TODO: Refactor this to pick up only needed fields and prevent more leaking
|
||||||
|
let apps = enabledApps.map(
|
||||||
|
({ credentials: _, credential: _1, key: _2 /* don't leak to frontend */, ...app }) => {
|
||||||
|
const credentialIds = credentials.filter((c) => c.type === app.type).map((c) => c.id);
|
||||||
|
const invalidCredentialIds = credentials
|
||||||
|
.filter((c) => c.type === app.type && c.invalid)
|
||||||
|
.map((c) => c.id);
|
||||||
|
return {
|
||||||
|
...app,
|
||||||
|
credentialIds,
|
||||||
|
invalidCredentialIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant) {
|
||||||
|
// `flatMap()` these work like `.filter()` but infers the types correctly
|
||||||
|
apps = apps
|
||||||
|
// variant check
|
||||||
|
.flatMap((item) => (item.variant.startsWith(variant) ? [item] : []));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclude) {
|
||||||
|
// exclusion filter
|
||||||
|
apps = apps.filter((item) => (exclude ? !exclude.includes(item.variant) : true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlyInstalled) {
|
||||||
|
apps = apps.flatMap((item) => (item.credentialIds.length > 0 || item.isGlobal ? [item] : []));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items: apps,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZIntegrationsInputSchema = z.object({
|
||||||
|
variant: z.string().optional(),
|
||||||
|
exclude: z.array(z.string()).optional(),
|
||||||
|
onlyInstalled: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIntegrationsInputSchema = z.infer<typeof ZIntegrationsInputSchema>;
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { getLocationGroupedOptions } from "@calcom/app-store/utils";
|
||||||
|
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||||
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
type LocationOptionsOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const locationOptionsHandler = async ({ ctx }: LocationOptionsOptions) => {
|
||||||
|
const credentials = await prisma.credential.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
type: true,
|
||||||
|
key: true,
|
||||||
|
userId: true,
|
||||||
|
appId: true,
|
||||||
|
invalid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrations = await getEnabledApps(credentials);
|
||||||
|
|
||||||
|
const t = await getTranslation(ctx.user.locale ?? "en", "common");
|
||||||
|
|
||||||
|
const locationOptions = getLocationGroupedOptions(integrations, t);
|
||||||
|
|
||||||
|
return locationOptions;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
type MeOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const meHandler = async ({ ctx }: MeOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
// Destructuring here only makes it more illegible
|
||||||
|
// pick only the part we want to expose in the API
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
startTime: user.startTime,
|
||||||
|
endTime: user.endTime,
|
||||||
|
bufferTime: user.bufferTime,
|
||||||
|
locale: user.locale,
|
||||||
|
timeFormat: user.timeFormat,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
avatar: user.avatar,
|
||||||
|
createdDate: user.createdDate,
|
||||||
|
trialEndsAt: user.trialEndsAt,
|
||||||
|
defaultScheduleId: user.defaultScheduleId,
|
||||||
|
completedOnboarding: user.completedOnboarding,
|
||||||
|
twoFactorEnabled: user.twoFactorEnabled,
|
||||||
|
disableImpersonation: user.disableImpersonation,
|
||||||
|
identityProvider: user.identityProvider,
|
||||||
|
brandColor: user.brandColor,
|
||||||
|
darkBrandColor: user.darkBrandColor,
|
||||||
|
away: user.away,
|
||||||
|
bio: user.bio,
|
||||||
|
weekStart: user.weekStart,
|
||||||
|
theme: user.theme,
|
||||||
|
hideBranding: user.hideBranding,
|
||||||
|
metadata: user.metadata,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
|
||||||
|
|
||||||
|
type SetDestinationCalendarOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TSetDestinationCalendarInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setDestinationCalendarHandler = async ({ ctx, input }: SetDestinationCalendarOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
const { integration, externalId, eventTypeId } = input;
|
||||||
|
const calendarCredentials = getCalendarCredentials(user.credentials);
|
||||||
|
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||||
|
const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat();
|
||||||
|
|
||||||
|
const credentialId = allCals.find(
|
||||||
|
(cal) => cal.externalId === externalId && cal.integration === integration && cal.readOnly === false
|
||||||
|
)?.credentialId;
|
||||||
|
|
||||||
|
if (!credentialId) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find calendar ${input.externalId}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
let where;
|
||||||
|
|
||||||
|
if (eventTypeId) {
|
||||||
|
if (
|
||||||
|
!(await prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: eventTypeId,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: `You don't have access to event type ${eventTypeId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
where = { eventTypeId };
|
||||||
|
} else where = { userId: user.id };
|
||||||
|
|
||||||
|
await prisma.destinationCalendar.upsert({
|
||||||
|
where,
|
||||||
|
update: {
|
||||||
|
integration,
|
||||||
|
externalId,
|
||||||
|
credentialId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
...where,
|
||||||
|
integration,
|
||||||
|
externalId,
|
||||||
|
credentialId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZSetDestinationCalendarInputSchema = z.object({
|
||||||
|
integration: z.string(),
|
||||||
|
externalId: z.string(),
|
||||||
|
eventTypeId: z.number().nullish(),
|
||||||
|
bookingId: z.number().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSetDestinationCalendarInputSchema = z.infer<typeof ZSetDestinationCalendarInputSchema>;
|
|
@ -0,0 +1,50 @@
|
||||||
|
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
type StripeCustomerOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stripeCustomerHandler = async ({ ctx }: StripeCustomerOptions) => {
|
||||||
|
const {
|
||||||
|
user: { id: userId },
|
||||||
|
} = ctx;
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
metadata: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = userMetadata.parse(user.metadata);
|
||||||
|
|
||||||
|
if (!metadata?.stripeCustomerId) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "No stripe customer id" });
|
||||||
|
}
|
||||||
|
// Fetch stripe customer
|
||||||
|
const stripeCustomerId = metadata?.stripeCustomerId;
|
||||||
|
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
if (customer.deleted) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "No stripe customer found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = customer?.metadata?.username || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPremium: !!metadata?.isPremium,
|
||||||
|
username,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { sendFeedbackEmail } from "@calcom/emails";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import type { TSubmitFeedbackInputSchema } from "./submitFeedback.schema";
|
||||||
|
|
||||||
|
type SubmitFeedbackOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TSubmitFeedbackInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const submitFeedbackHandler = async ({ ctx, input }: SubmitFeedbackOptions) => {
|
||||||
|
const { rating, comment } = input;
|
||||||
|
|
||||||
|
const feedback = {
|
||||||
|
username: ctx.user.username || "Nameless",
|
||||||
|
email: ctx.user.email || "No email address",
|
||||||
|
rating: rating,
|
||||||
|
comment: comment,
|
||||||
|
};
|
||||||
|
|
||||||
|
await prisma.feedback.create({
|
||||||
|
data: {
|
||||||
|
date: dayjs().toISOString(),
|
||||||
|
userId: ctx.user.id,
|
||||||
|
rating: rating,
|
||||||
|
comment: comment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.SEND_FEEDBACK_EMAIL && comment) sendFeedbackEmail(feedback);
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZSubmitFeedbackInputSchema = z.object({
|
||||||
|
rating: z.string(),
|
||||||
|
comment: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSubmitFeedbackInputSchema = z.infer<typeof ZSubmitFeedbackInputSchema>;
|
|
@ -0,0 +1,140 @@
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
|
import type { NextApiResponse, GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||||
|
import { getPremiumPlanProductId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
|
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||||
|
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||||
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TUpdateProfileInputSchema } from "./updateProfile.schema";
|
||||||
|
|
||||||
|
type UpdateProfileOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
res?: NextApiResponse | GetServerSidePropsContext["res"];
|
||||||
|
};
|
||||||
|
input: TUpdateProfileInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
const data: Prisma.UserUpdateInput = {
|
||||||
|
...input,
|
||||||
|
metadata: input.metadata as Prisma.InputJsonValue,
|
||||||
|
};
|
||||||
|
let isPremiumUsername = false;
|
||||||
|
if (input.username) {
|
||||||
|
const username = slugify(input.username);
|
||||||
|
// Only validate if we're changing usernames
|
||||||
|
if (username !== user.username) {
|
||||||
|
data.username = username;
|
||||||
|
const response = await checkUsername(username);
|
||||||
|
isPremiumUsername = response.premium;
|
||||||
|
if (!response.available) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: response.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (input.avatar) {
|
||||||
|
data.avatar = await resizeBase64Image(input.avatar);
|
||||||
|
}
|
||||||
|
const userToUpdate = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userToUpdate) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||||
|
}
|
||||||
|
const metadata = userMetadata.parse(userToUpdate.metadata);
|
||||||
|
|
||||||
|
const isPremium = metadata?.isPremium;
|
||||||
|
if (isPremiumUsername) {
|
||||||
|
const stripeCustomerId = metadata?.stripeCustomerId;
|
||||||
|
if (!isPremium || !stripeCustomerId) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "User is not premium" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripeSubscriptions = await stripe.subscriptions.list({ customer: stripeCustomerId });
|
||||||
|
|
||||||
|
if (!stripeSubscriptions || !stripeSubscriptions.data.length) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "No stripeSubscription found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over subscriptions and look for premium product id and status active
|
||||||
|
// @TODO: iterate if stripeSubscriptions.hasMore is true
|
||||||
|
const isPremiumUsernameSubscriptionActive = stripeSubscriptions.data.some(
|
||||||
|
(subscription) =>
|
||||||
|
subscription.items.data[0].price.product === getPremiumPlanProductId() &&
|
||||||
|
subscription.status === "active"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isPremiumUsernameSubscriptionActive) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "You need to pay for premium username",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
metadata: true,
|
||||||
|
name: true,
|
||||||
|
createdDate: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync Services
|
||||||
|
await syncServicesUpdateWebUser(updatedUser);
|
||||||
|
|
||||||
|
// Notify stripe about the change
|
||||||
|
if (updatedUser && updatedUser.metadata && hasKeyInMetadata(updatedUser, "stripeCustomerId")) {
|
||||||
|
const stripeCustomerId = `${updatedUser.metadata.stripeCustomerId}`;
|
||||||
|
await stripe.customers.update(stripeCustomerId, {
|
||||||
|
metadata: {
|
||||||
|
username: updatedUser.username,
|
||||||
|
email: updatedUser.email,
|
||||||
|
userId: updatedUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Revalidate booking pages
|
||||||
|
const res = ctx.res as NextApiResponse;
|
||||||
|
if (typeof res?.revalidate !== "undefined") {
|
||||||
|
const eventTypes = await prisma.eventType.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
team: null,
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// waiting for this isn't needed
|
||||||
|
Promise.all(eventTypes.map((eventType) => res?.revalidate(`/${ctx.user.username}/${eventType.slug}`)))
|
||||||
|
.then(() => console.info("Booking pages revalidated"))
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||||
|
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
export const ZUpdateProfileInputSchema = z.object({
|
||||||
|
username: z.string().optional(),
|
||||||
|
name: z.string().max(FULL_NAME_LENGTH_MAX_LIMIT).optional(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
bio: z.string().optional(),
|
||||||
|
avatar: z.string().optional(),
|
||||||
|
timeZone: z.string().optional(),
|
||||||
|
weekStart: z.string().optional(),
|
||||||
|
hideBranding: z.boolean().optional(),
|
||||||
|
allowDynamicBooking: z.boolean().optional(),
|
||||||
|
brandColor: z.string().optional(),
|
||||||
|
darkBrandColor: z.string().optional(),
|
||||||
|
theme: z.string().optional().nullable(),
|
||||||
|
completedOnboarding: z.boolean().optional(),
|
||||||
|
locale: z.string().optional(),
|
||||||
|
timeFormat: z.number().optional(),
|
||||||
|
disableImpersonation: z.boolean().optional(),
|
||||||
|
metadata: userMetadata.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TUpdateProfileInputSchema = z.infer<typeof ZUpdateProfileInputSchema>;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import getApps from "@calcom/app-store/utils";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
|
||||||
|
|
||||||
|
type UpdateUserDefaultConferencingAppOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TUpdateUserDefaultConferencingAppInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserDefaultConferencingAppHandler = async ({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
}: UpdateUserDefaultConferencingAppOptions) => {
|
||||||
|
const currentMetadata = userMetadata.parse(ctx.user.metadata);
|
||||||
|
const credentials = ctx.user.credentials;
|
||||||
|
const foundApp = getApps(credentials).filter((app) => app.slug === input.appSlug)[0];
|
||||||
|
const appLocation = foundApp?.appData?.location;
|
||||||
|
|
||||||
|
if (!foundApp || !appLocation) throw new TRPCError({ code: "BAD_REQUEST", message: "App not installed" });
|
||||||
|
|
||||||
|
if (appLocation.linkType === "static" && !input.appLink) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "App link is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appLocation.linkType === "static" && appLocation.urlRegExp) {
|
||||||
|
const validLink = z
|
||||||
|
.string()
|
||||||
|
.regex(new RegExp(appLocation.urlRegExp), "Invalid App Link")
|
||||||
|
.parse(input.appLink);
|
||||||
|
if (!validLink) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid app link" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
metadata: {
|
||||||
|
...currentMetadata,
|
||||||
|
defaultConferencingApp: {
|
||||||
|
appSlug: input.appSlug,
|
||||||
|
appLink: input.appLink,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return input;
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZUpdateUserDefaultConferencingAppInputSchema = z.object({
|
||||||
|
appSlug: z.string().optional(),
|
||||||
|
appLink: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TUpdateUserDefaultConferencingAppInputSchema = z.infer<
|
||||||
|
typeof ZUpdateUserDefaultConferencingAppInputSchema
|
||||||
|
>;
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { publicProcedure, router } from "../../trpc";
|
||||||
|
import { slotsRouter } from "../viewer/slots/_router";
|
||||||
|
import { ZEventInputSchema } from "./event.schema";
|
||||||
|
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||||
|
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
|
||||||
|
|
||||||
|
type PublicViewerRouterHandlerCache = {
|
||||||
|
session?: typeof import("./session.handler").sessionHandler;
|
||||||
|
i18n?: typeof import("./i18n.handler").i18nHandler;
|
||||||
|
countryCode?: typeof import("./countryCode.handler").countryCodeHandler;
|
||||||
|
samlTenantProduct?: typeof import("./samlTenantProduct.handler").samlTenantProductHandler;
|
||||||
|
stripeCheckoutSession?: typeof import("./stripeCheckoutSession.handler").stripeCheckoutSessionHandler;
|
||||||
|
cityTimezones?: typeof import("./cityTimezones.handler").cityTimezonesHandler;
|
||||||
|
event?: typeof import("./event.handler").eventHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNSTABLE_HANDLER_CACHE: PublicViewerRouterHandlerCache = {};
|
||||||
|
|
||||||
|
// things that unauthenticated users can query about themselves
|
||||||
|
export const publicViewerRouter = router({
|
||||||
|
session: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.session) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.session = await import("./session.handler").then((mod) => mod.sessionHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.session) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.session({
|
||||||
|
ctx,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
i18n: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.i18n) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.i18n = await import("./i18n.handler").then((mod) => mod.i18nHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.i18n) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.i18n({
|
||||||
|
ctx,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
countryCode: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.countryCode) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.countryCode = await import("./countryCode.handler").then(
|
||||||
|
(mod) => mod.countryCodeHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.countryCode) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.countryCode({
|
||||||
|
ctx,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.samlTenantProduct) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.samlTenantProduct = await import("./samlTenantProduct.handler").then(
|
||||||
|
(mod) => mod.samlTenantProductHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.samlTenantProduct) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.samlTenantProduct({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
stripeCheckoutSession: publicProcedure
|
||||||
|
.input(ZStripeCheckoutSessionInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.stripeCheckoutSession) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.stripeCheckoutSession = await import("./stripeCheckoutSession.handler").then(
|
||||||
|
(mod) => mod.stripeCheckoutSessionHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.stripeCheckoutSession) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.stripeCheckoutSession({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
cityTimezones: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.cityTimezones) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.cityTimezones = await import("./cityTimezones.handler").then(
|
||||||
|
(mod) => mod.cityTimezonesHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.cityTimezones) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.cityTimezones({
|
||||||
|
ctx,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// REVIEW: This router is part of both the public and private viewer router?
|
||||||
|
slots: slotsRouter,
|
||||||
|
event: publicProcedure.input(ZEventInputSchema).query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.event) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.event = await import("./event.handler").then((mod) => mod.eventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.event) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.event({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
type CityTimezonesOptions = {
|
||||||
|
ctx: {
|
||||||
|
session: Session | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cityTimezonesHandler = async ({ ctx: _ctx }: CityTimezonesOptions) => {
|
||||||
|
/**
|
||||||
|
* Lazy loads third party dependency to avoid loading 1.5Mb for ALL tRPC procedures.
|
||||||
|
* Thanks @roae for the tip 🙏
|
||||||
|
**/
|
||||||
|
const allCities = await import("city-timezones").then((mod) => mod.cityMapping);
|
||||||
|
/**
|
||||||
|
* Filter out all cities that have the same "city" key and only use the one with the highest population.
|
||||||
|
* This way we return a new array of cities without running the risk of having more than one city
|
||||||
|
* with the same name on the dropdown and prevent users from mistaking the time zone of the desired city.
|
||||||
|
*/
|
||||||
|
const topPopulatedCities: { [key: string]: { city: string; timezone: string; pop: number } } = {};
|
||||||
|
allCities.forEach((city) => {
|
||||||
|
const cityPopulationCount = city.pop;
|
||||||
|
if (
|
||||||
|
topPopulatedCities[city.city]?.pop === undefined ||
|
||||||
|
cityPopulationCount > topPopulatedCities[city.city].pop
|
||||||
|
) {
|
||||||
|
topPopulatedCities[city.city] = { city: city.city, timezone: city.timezone, pop: city.pop };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const uniqueCities = Object.values(topPopulatedCities);
|
||||||
|
/** Add specific overries in here */
|
||||||
|
uniqueCities.forEach((city) => {
|
||||||
|
if (city.city === "London") city.timezone = "Europe/London";
|
||||||
|
if (city.city === "Londonderry") city.city = "London";
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueCities;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { CreateInnerContextOptions } from "../../createContext";
|
||||||
|
|
||||||
|
type CountryCodeOptions = {
|
||||||
|
ctx: CreateInnerContextOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const countryCodeHandler = async ({ ctx }: CountryCodeOptions) => {
|
||||||
|
const { req } = ctx;
|
||||||
|
|
||||||
|
const countryCode: string | string[] = req?.headers?.["x-vercel-ip-country"] ?? "";
|
||||||
|
return { countryCode: Array.isArray(countryCode) ? countryCode[0] : countryCode };
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||||
|
|
||||||
|
import type { TEventInputSchema } from "./event.schema";
|
||||||
|
|
||||||
|
interface EventHandlerOptions {
|
||||||
|
ctx: { prisma: PrismaClient };
|
||||||
|
input: TEventInputSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventHandler = async ({ ctx, input }: EventHandlerOptions) => {
|
||||||
|
const event = await getPublicEvent(input.username, input.eventSlug, ctx.prisma);
|
||||||
|
return event;
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export const ZEventInputSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
eventSlug: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TEventInputSchema = z.infer<typeof ZEventInputSchema>;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { CreateInnerContextOptions } from "../../createContext";
|
||||||
|
import { getLocale } from "../../trpc";
|
||||||
|
|
||||||
|
type I18nOptions = {
|
||||||
|
ctx: CreateInnerContextOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const i18nHandler = async ({ ctx }: I18nOptions) => {
|
||||||
|
const { locale, i18n } = await getLocale(ctx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||||
|
|
||||||
|
type SamlTenantProductOptions = {
|
||||||
|
ctx: {
|
||||||
|
session: Session | null;
|
||||||
|
};
|
||||||
|
input: TSamlTenantProductInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const samlTenantProductHandler = async ({ ctx: _ctx, input }: SamlTenantProductOptions) => {
|
||||||
|
const { email } = input;
|
||||||
|
|
||||||
|
return await samlTenantProduct(prisma, email);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZSamlTenantProductInputSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSamlTenantProductInputSchema = z.infer<typeof ZSamlTenantProductInputSchema>;
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
type SessionOptions = {
|
||||||
|
ctx: {
|
||||||
|
session: Session | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sessionHandler = async ({ ctx }: SessionOptions) => {
|
||||||
|
return ctx.session;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,72 @@
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||||
|
|
||||||
|
import type { TStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
|
||||||
|
|
||||||
|
type StripeCheckoutSessionOptions = {
|
||||||
|
ctx: {
|
||||||
|
session: Session | null;
|
||||||
|
};
|
||||||
|
input: TStripeCheckoutSessionInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stripeCheckoutSessionHandler = async ({ input }: StripeCheckoutSessionOptions) => {
|
||||||
|
const { checkoutSessionId, stripeCustomerId } = input;
|
||||||
|
|
||||||
|
// TODO: Move the following data checks to superRefine
|
||||||
|
if (!checkoutSessionId && !stripeCustomerId) {
|
||||||
|
throw new Error("Missing checkoutSessionId or stripeCustomerId");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkoutSessionId && stripeCustomerId) {
|
||||||
|
throw new Error("Both checkoutSessionId and stripeCustomerId provided");
|
||||||
|
}
|
||||||
|
let customerId: string;
|
||||||
|
let isPremiumUsername = false;
|
||||||
|
let hasPaymentFailed = false;
|
||||||
|
if (checkoutSessionId) {
|
||||||
|
try {
|
||||||
|
const session = await stripe.checkout.sessions.retrieve(checkoutSessionId);
|
||||||
|
if (typeof session.customer !== "string") {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
customerId = session.customer;
|
||||||
|
isPremiumUsername = true;
|
||||||
|
hasPaymentFailed = session.payment_status !== "paid";
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
customerId = stripeCustomerId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const customer = await stripe.customers.retrieve(customerId);
|
||||||
|
if (customer.deleted) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
hasPaymentFailed,
|
||||||
|
isPremiumUsername,
|
||||||
|
customer: {
|
||||||
|
username: customer.metadata.username,
|
||||||
|
email: customer.metadata.email,
|
||||||
|
stripeCustomerId: customerId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZStripeCheckoutSessionInputSchema = z.object({
|
||||||
|
stripeCustomerId: z.string().optional(),
|
||||||
|
checkoutSessionId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TStripeCheckoutSessionInputSchema = z.infer<typeof ZStripeCheckoutSessionInputSchema>;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,50 @@
|
||||||
|
import ethRouter from "@calcom/app-store/rainbow/trpc/router";
|
||||||
|
import app_RoutingForms from "@calcom/app-store/routing-forms/trpc-router";
|
||||||
|
import { userAdminRouter } from "@calcom/features/ee/users/server/trpc-router";
|
||||||
|
import { featureFlagRouter } from "@calcom/features/flags/server/router";
|
||||||
|
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";
|
||||||
|
|
||||||
|
import { mergeRouters, router } from "../../trpc";
|
||||||
|
import { loggedInViewerRouter } from "../loggedInViewer/_router";
|
||||||
|
import { publicViewerRouter } from "../publicViewer/_router";
|
||||||
|
import { apiKeysRouter } from "./apiKeys/_router";
|
||||||
|
import { appsRouter } from "./apps/_router";
|
||||||
|
import { authRouter } from "./auth/_router";
|
||||||
|
import { availabilityRouter } from "./availability/_router";
|
||||||
|
import { bookingsRouter } from "./bookings/_router";
|
||||||
|
import { deploymentSetupRouter } from "./deploymentSetup/_router";
|
||||||
|
import { eventTypesRouter } from "./eventTypes/_router";
|
||||||
|
import { paymentsRouter } from "./payments/_router";
|
||||||
|
import { slotsRouter } from "./slots/_router";
|
||||||
|
import { ssoRouter } from "./sso/_router";
|
||||||
|
import { viewerTeamsRouter } from "./teams/_router";
|
||||||
|
import { webhookRouter } from "./webhook/_router";
|
||||||
|
import { workflowsRouter } from "./workflows/_router";
|
||||||
|
|
||||||
|
export const viewerRouter = mergeRouters(
|
||||||
|
loggedInViewerRouter,
|
||||||
|
router({
|
||||||
|
loggedInViewerRouter,
|
||||||
|
public: publicViewerRouter,
|
||||||
|
auth: authRouter,
|
||||||
|
deploymentSetup: deploymentSetupRouter,
|
||||||
|
bookings: bookingsRouter,
|
||||||
|
eventTypes: eventTypesRouter,
|
||||||
|
availability: availabilityRouter,
|
||||||
|
teams: viewerTeamsRouter,
|
||||||
|
webhook: webhookRouter,
|
||||||
|
apiKeys: apiKeysRouter,
|
||||||
|
slots: slotsRouter,
|
||||||
|
workflows: workflowsRouter,
|
||||||
|
saml: ssoRouter,
|
||||||
|
insights: insightsRouter,
|
||||||
|
payments: paymentsRouter,
|
||||||
|
// NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved.
|
||||||
|
// After that there would just one merge call here for all the apps.
|
||||||
|
appRoutingForms: app_RoutingForms,
|
||||||
|
eth: ethRouter,
|
||||||
|
features: featureFlagRouter,
|
||||||
|
appsRouter,
|
||||||
|
users: userAdminRouter,
|
||||||
|
})
|
||||||
|
);
|
|
@ -1,153 +0,0 @@
|
||||||
import { v4 } from "uuid";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { generateUniqueAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
|
||||||
|
|
||||||
import { router, authedProcedure } from "../../trpc";
|
|
||||||
|
|
||||||
export const apiKeysRouter = router({
|
|
||||||
list: authedProcedure.query(async ({ ctx }) => {
|
|
||||||
return await ctx.prisma.apiKey.findMany({
|
|
||||||
where: {
|
|
||||||
userId: ctx.user.id,
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
NOT: {
|
|
||||||
appId: "zapier",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
appId: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
findKeyOfType: authedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
appId: z.string().optional().nullable(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
return await ctx.prisma.apiKey.findFirst({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
userId: ctx.user.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
appId: input.appId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
create: authedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
note: z.string().optional().nullish(),
|
|
||||||
expiresAt: z.date().optional().nullable(),
|
|
||||||
neverExpires: z.boolean().optional(),
|
|
||||||
appId: z.string().optional().nullable(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const [hashedApiKey, apiKey] = generateUniqueAPIKey();
|
|
||||||
// Here we snap never expires before deleting it so it's not passed to prisma create call.
|
|
||||||
const neverExpires = input.neverExpires;
|
|
||||||
delete input.neverExpires;
|
|
||||||
await ctx.prisma.apiKey.create({
|
|
||||||
data: {
|
|
||||||
id: v4(),
|
|
||||||
userId: ctx.user.id,
|
|
||||||
...input,
|
|
||||||
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
|
|
||||||
expiresAt: neverExpires ? null : input.expiresAt,
|
|
||||||
hashedKey: hashedApiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const prefixedApiKey = `${process.env.API_KEY_PREFIX ?? "cal_"}${apiKey}`;
|
|
||||||
return prefixedApiKey;
|
|
||||||
}),
|
|
||||||
edit: authedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
note: z.string().optional().nullish(),
|
|
||||||
expiresAt: z.date().optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { id, ...data } = input;
|
|
||||||
const {
|
|
||||||
apiKeys: [updatedApiKey],
|
|
||||||
} = await ctx.prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: ctx.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiKeys: {
|
|
||||||
update: {
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
apiKeys: {
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return updatedApiKey;
|
|
||||||
}),
|
|
||||||
delete: authedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
eventTypeId: z.number().optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { id } = input;
|
|
||||||
|
|
||||||
const apiKeyToDelete = await ctx.prisma.apiKey.findFirst({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await ctx.prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: ctx.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiKeys: {
|
|
||||||
delete: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//remove all existing zapier webhooks, as we always have only one zapier API key and the running zaps won't work any more if this key is deleted
|
|
||||||
if (apiKeyToDelete && apiKeyToDelete.appId === "zapier") {
|
|
||||||
await ctx.prisma.webhook.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: ctx.user.id,
|
|
||||||
appId: "zapier",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { authedProcedure, router } from "../../../trpc";
|
||||||
|
import { ZCreateInputSchema } from "./create.schema";
|
||||||
|
import { ZDeleteInputSchema } from "./delete.schema";
|
||||||
|
import { ZEditInputSchema } from "./edit.schema";
|
||||||
|
import { ZFindKeyOfTypeInputSchema } from "./findKeyOfType.schema";
|
||||||
|
|
||||||
|
type ApiKeysRouterHandlerCache = {
|
||||||
|
list?: typeof import("./list.handler").listHandler;
|
||||||
|
findKeyOfType?: typeof import("./findKeyOfType.handler").findKeyOfTypeHandler;
|
||||||
|
create?: typeof import("./create.handler").createHandler;
|
||||||
|
edit?: typeof import("./edit.handler").editHandler;
|
||||||
|
delete?: typeof import("./delete.handler").deleteHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNSTABLE_HANDLER_CACHE: ApiKeysRouterHandlerCache = {};
|
||||||
|
|
||||||
|
export const apiKeysRouter = router({
|
||||||
|
// List keys
|
||||||
|
list: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.list) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.list = await import("./list.handler").then((mod) => mod.listHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.list) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.list({
|
||||||
|
ctx,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Find key of type
|
||||||
|
findKeyOfType: authedProcedure.input(ZFindKeyOfTypeInputSchema).query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.findKeyOfType) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.findKeyOfType = await import("./findKeyOfType.handler").then(
|
||||||
|
(mod) => mod.findKeyOfTypeHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.findKeyOfType) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.findKeyOfType({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Create a new key
|
||||||
|
create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.create) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.create = await import("./create.handler").then((mod) => mod.createHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.create) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.create({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
edit: authedProcedure.input(ZEditInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.edit) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.edit = await import("./edit.handler").then((mod) => mod.editHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.edit) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.edit({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: authedProcedure.input(ZDeleteInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.delete) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.delete = await import("./delete.handler").then((mod) => mod.deleteHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.delete) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.delete({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
|
import { generateUniqueAPIKey } from "@calcom/ee/api-keys/lib/apiKeys";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
import type { TCreateInputSchema } from "./create.schema";
|
||||||
|
|
||||||
|
type CreateHandlerOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TCreateInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createHandler = async ({ ctx, input }: CreateHandlerOptions) => {
|
||||||
|
const [hashedApiKey, apiKey] = generateUniqueAPIKey();
|
||||||
|
|
||||||
|
// Here we snap never expires before deleting it so it's not passed to prisma create call.
|
||||||
|
const { neverExpires, ...rest } = input;
|
||||||
|
|
||||||
|
await prisma.apiKey.create({
|
||||||
|
data: {
|
||||||
|
id: v4(),
|
||||||
|
userId: ctx.user.id,
|
||||||
|
...rest,
|
||||||
|
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
|
||||||
|
expiresAt: neverExpires ? null : rest.expiresAt,
|
||||||
|
hashedKey: hashedApiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiKeyPrefix = process.env.API_KEY_PREFIX ?? "cal_";
|
||||||
|
|
||||||
|
const prefixedApiKey = `${apiKeyPrefix}${apiKey}`;
|
||||||
|
|
||||||
|
return prefixedApiKey;
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZCreateInputSchema = z.object({
|
||||||
|
note: z.string().optional().nullish(),
|
||||||
|
expiresAt: z.date().optional().nullable(),
|
||||||
|
neverExpires: z.boolean().optional(),
|
||||||
|
appId: z.string().optional().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCreateInputSchema = z.infer<typeof ZCreateInputSchema>;
|
|
@ -0,0 +1,48 @@
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
import type { TDeleteInputSchema } from "./delete.schema";
|
||||||
|
|
||||||
|
type DeleteOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TDeleteInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteHandler = async ({ ctx, input }: DeleteOptions) => {
|
||||||
|
const { id } = input;
|
||||||
|
|
||||||
|
const apiKeyToDelete = await prisma.apiKey.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
apiKeys: {
|
||||||
|
delete: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//remove all existing zapier webhooks, as we always have only one zapier API key and the running zaps won't work any more if this key is deleted
|
||||||
|
if (apiKeyToDelete && apiKeyToDelete.appId === "zapier") {
|
||||||
|
await prisma.webhook.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
appId: "zapier",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZDeleteInputSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
eventTypeId: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TDeleteInputSchema = z.infer<typeof ZDeleteInputSchema>;
|
|
@ -0,0 +1,42 @@
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
import type { TEditInputSchema } from "./edit.schema";
|
||||||
|
|
||||||
|
type EditOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TEditInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editHandler = async ({ ctx, input }: EditOptions) => {
|
||||||
|
const { id, ...data } = input;
|
||||||
|
|
||||||
|
const {
|
||||||
|
apiKeys: [updatedApiKey],
|
||||||
|
} = await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
apiKeys: {
|
||||||
|
update: {
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
apiKeys: {
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedApiKey;
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZEditInputSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
note: z.string().optional().nullish(),
|
||||||
|
expiresAt: z.date().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TEditInputSchema = z.infer<typeof ZEditInputSchema>;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
import type { TFindKeyOfTypeInputSchema } from "./findKeyOfType.schema";
|
||||||
|
|
||||||
|
type FindKeyOfTypeOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TFindKeyOfTypeInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findKeyOfTypeHandler = async ({ ctx, input }: FindKeyOfTypeOptions) => {
|
||||||
|
return await prisma.apiKey.findFirst({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: input.appId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZFindKeyOfTypeInputSchema = z.object({
|
||||||
|
appId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFindKeyOfTypeInputSchema = z.infer<typeof ZFindKeyOfTypeInputSchema>;
|
|
@ -0,0 +1,28 @@
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
|
||||||
|
type ListOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listHandler = async ({ ctx }: ListOptions) => {
|
||||||
|
return await prisma.apiKey.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
NOT: {
|
||||||
|
appId: "zapier",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -1,357 +0,0 @@
|
||||||
import { AppCategories } from "@prisma/client";
|
|
||||||
import type { Prisma } from "@prisma/client";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
|
||||||
import { getLocalAppMetadata, getAppFromSlug } from "@calcom/app-store/utils";
|
|
||||||
import { sendDisabledAppEmail } from "@calcom/emails";
|
|
||||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
|
|
||||||
import { authedAdminProcedure, authedProcedure, router } from "../../trpc";
|
|
||||||
|
|
||||||
export const appsRouter = router({
|
|
||||||
listLocal: authedAdminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
category: z.nativeEnum({ ...AppCategories, conferencing: "conferencing" }),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const category = input.category === "conferencing" ? "video" : input.category;
|
|
||||||
const localApps = getLocalAppMetadata();
|
|
||||||
|
|
||||||
const dbApps = await ctx.prisma.app.findMany({
|
|
||||||
where: {
|
|
||||||
categories: {
|
|
||||||
has: AppCategories[category as keyof typeof AppCategories],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
slug: true,
|
|
||||||
keys: true,
|
|
||||||
enabled: true,
|
|
||||||
dirName: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return localApps.flatMap((app) => {
|
|
||||||
// Filter applications that does not belong to the current requested category.
|
|
||||||
if (!(app.category === category || app.categories?.some((appCategory) => appCategory === category))) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find app metadata
|
|
||||||
const dbData = dbApps.find((dbApp) => dbApp.slug === app.slug);
|
|
||||||
|
|
||||||
// If the app already contains keys then return
|
|
||||||
if (dbData?.keys) {
|
|
||||||
return {
|
|
||||||
name: app.name,
|
|
||||||
slug: app.slug,
|
|
||||||
logo: app.logo,
|
|
||||||
title: app.title,
|
|
||||||
type: app.type,
|
|
||||||
description: app.description,
|
|
||||||
// We know that keys are going to be an object or null. Prisma can not type check against JSON fields
|
|
||||||
keys: dbData.keys as Prisma.JsonObject | null,
|
|
||||||
dirName: app.dirName || app.slug,
|
|
||||||
enabled: dbData?.enabled || false,
|
|
||||||
isTemplate: app.isTemplate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const keysSchema = appKeysSchemas[app.dirName as keyof typeof appKeysSchemas];
|
|
||||||
|
|
||||||
const keys: Record<string, string> = {};
|
|
||||||
|
|
||||||
// `typeof val === 'undefined'` is always slower than !== undefined comparison
|
|
||||||
// it is important to avoid string to string comparisons as much as we can
|
|
||||||
if (keysSchema !== undefined) {
|
|
||||||
// TODO: Remove the Object.values and reduce to improve the performance.
|
|
||||||
Object.values(keysSchema.keyof()._def.values).reduce((keysObject, key) => {
|
|
||||||
keys[key as string] = "";
|
|
||||||
return keysObject;
|
|
||||||
}, {} as Record<string, string>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: app.name,
|
|
||||||
slug: app.slug,
|
|
||||||
logo: app.logo,
|
|
||||||
type: app.type,
|
|
||||||
title: app.title,
|
|
||||||
description: app.description,
|
|
||||||
enabled: dbData?.enabled ?? false,
|
|
||||||
dirName: app.dirName ?? app.slug,
|
|
||||||
keys: Object.keys(keys).length === 0 ? null : keys,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
toggle: authedAdminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
slug: z.string(),
|
|
||||||
enabled: z.boolean(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { prisma } = ctx;
|
|
||||||
const { enabled } = input;
|
|
||||||
|
|
||||||
// Get app name from metadata
|
|
||||||
const localApps = getLocalAppMetadata();
|
|
||||||
const appMetadata = localApps.find((localApp) => localApp.slug === input.slug);
|
|
||||||
|
|
||||||
if (!appMetadata)
|
|
||||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "App metadata could not be found" });
|
|
||||||
|
|
||||||
const app = await prisma.app.upsert({
|
|
||||||
where: {
|
|
||||||
slug: input.slug,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
enabled,
|
|
||||||
dirName: appMetadata?.dirName || appMetadata?.slug || "",
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
slug: input.slug,
|
|
||||||
dirName: appMetadata?.dirName || appMetadata?.slug || "",
|
|
||||||
categories:
|
|
||||||
(appMetadata?.categories as AppCategories[]) ||
|
|
||||||
([appMetadata?.category] as AppCategories[]) ||
|
|
||||||
undefined,
|
|
||||||
keys: undefined,
|
|
||||||
enabled,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// If disabling an app then we need to alert users based on the app type
|
|
||||||
if (!enabled) {
|
|
||||||
const translations = new Map();
|
|
||||||
|
|
||||||
if (app.categories.some((category) => ["calendar", "video"].includes(category))) {
|
|
||||||
// Find all users with the app credentials
|
|
||||||
const appCredentials = await prisma.credential.findMany({
|
|
||||||
where: {
|
|
||||||
appId: app.slug,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
locale: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This should be done async probably using a queue.
|
|
||||||
Promise.all(
|
|
||||||
appCredentials.map(async (credential) => {
|
|
||||||
// No need to continue if credential does not have a user
|
|
||||||
if (!credential.user || !credential.user.email) return;
|
|
||||||
|
|
||||||
const locale = credential.user.locale ?? "en";
|
|
||||||
let t = translations.get(locale);
|
|
||||||
|
|
||||||
if (!t) {
|
|
||||||
t = await getTranslation(locale, "common");
|
|
||||||
translations.set(locale, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendDisabledAppEmail({
|
|
||||||
email: credential.user.email,
|
|
||||||
appName: appMetadata?.name || app.slug,
|
|
||||||
appType: app.categories,
|
|
||||||
t,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const eventTypesWithApp = await prisma.eventType.findMany({
|
|
||||||
where: {
|
|
||||||
metadata: {
|
|
||||||
path: ["apps", app.slug as string, "enabled"],
|
|
||||||
equals: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
users: {
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
locale: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This should be done async probably using a queue.
|
|
||||||
Promise.all(
|
|
||||||
eventTypesWithApp.map(async (eventType) => {
|
|
||||||
// TODO: This update query can be removed by merging it with
|
|
||||||
// the previous `findMany` query, if that query returns certain values.
|
|
||||||
await prisma.eventType.update({
|
|
||||||
where: {
|
|
||||||
id: eventType.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
metadata: {
|
|
||||||
...(eventType.metadata as object),
|
|
||||||
apps: {
|
|
||||||
// From this comment we can not type JSON fields in Prisma https://github.com/prisma/prisma/issues/3219#issuecomment-670202980
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-ignore
|
|
||||||
...eventType.metadata?.apps,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-ignore
|
|
||||||
[app.slug]: { ...eventType.metadata?.apps[app.slug], enabled: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
eventType.users.map(async (user) => {
|
|
||||||
const locale = user.locale ?? "en";
|
|
||||||
let t = translations.get(locale);
|
|
||||||
|
|
||||||
if (!t) {
|
|
||||||
t = await getTranslation(locale, "common");
|
|
||||||
translations.set(locale, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendDisabledAppEmail({
|
|
||||||
email: user.email,
|
|
||||||
appName: appMetadata?.name || app.slug,
|
|
||||||
appType: app.categories,
|
|
||||||
t,
|
|
||||||
title: eventType.title,
|
|
||||||
eventTypeId: eventType.id,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.enabled;
|
|
||||||
}),
|
|
||||||
saveKeys: authedAdminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
slug: z.string(),
|
|
||||||
dirName: z.string(),
|
|
||||||
type: z.string(),
|
|
||||||
// Validate w/ app specific schema
|
|
||||||
keys: z.unknown(),
|
|
||||||
fromEnabled: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const keysSchema = appKeysSchemas[input.dirName as keyof typeof appKeysSchemas];
|
|
||||||
const keys = keysSchema.parse(input.keys);
|
|
||||||
|
|
||||||
// Get app name from metadata
|
|
||||||
const localApps = getLocalAppMetadata();
|
|
||||||
const appMetadata = localApps.find((localApp) => localApp.slug === input.slug);
|
|
||||||
|
|
||||||
if (!appMetadata?.dirName && appMetadata?.categories)
|
|
||||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "App metadata could not be found" });
|
|
||||||
|
|
||||||
await ctx.prisma.app.upsert({
|
|
||||||
where: {
|
|
||||||
slug: input.slug,
|
|
||||||
},
|
|
||||||
update: { keys, ...(input.fromEnabled && { enabled: true }) },
|
|
||||||
create: {
|
|
||||||
slug: input.slug,
|
|
||||||
dirName: appMetadata?.dirName || appMetadata?.slug || "",
|
|
||||||
categories:
|
|
||||||
(appMetadata?.categories as AppCategories[]) ||
|
|
||||||
([appMetadata?.category] as AppCategories[]) ||
|
|
||||||
undefined,
|
|
||||||
keys: (input.keys as Prisma.InputJsonObject) || undefined,
|
|
||||||
...(input.fromEnabled && { enabled: true }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
checkForGCal: authedProcedure.query(async ({ ctx }) => {
|
|
||||||
const gCalPresent = await ctx.prisma.credential.findFirst({
|
|
||||||
where: {
|
|
||||||
type: "google_calendar",
|
|
||||||
userId: ctx.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return !!gCalPresent;
|
|
||||||
}),
|
|
||||||
updateAppCredentials: authedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
credentialId: z.number(),
|
|
||||||
key: z.object({}).passthrough(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { user } = ctx;
|
|
||||||
|
|
||||||
const { key } = input;
|
|
||||||
|
|
||||||
// Find user credential
|
|
||||||
const credential = await ctx.prisma.credential.findFirst({
|
|
||||||
where: {
|
|
||||||
id: input.credentialId,
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Check if credential exists
|
|
||||||
if (!credential) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Could not find credential ${input.credentialId}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await ctx.prisma.credential.update({
|
|
||||||
where: {
|
|
||||||
id: credential.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
key: {
|
|
||||||
...(credential.key as Prisma.JsonObject),
|
|
||||||
...(key as Prisma.JsonObject),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return !!updated;
|
|
||||||
}),
|
|
||||||
queryForDependencies: authedProcedure.input(z.string().array().optional()).query(async ({ ctx, input }) => {
|
|
||||||
if (!input) return;
|
|
||||||
|
|
||||||
const dependencyData: { name: string; slug: string; installed: boolean }[] = [];
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
input.map(async (dependency) => {
|
|
||||||
const appInstalled = await ctx.prisma.credential.findFirst({
|
|
||||||
where: {
|
|
||||||
appId: dependency,
|
|
||||||
userId: ctx.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = await getAppFromSlug(dependency);
|
|
||||||
|
|
||||||
dependencyData.push({ name: app?.name || dependency, slug: dependency, installed: !!appInstalled });
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return dependencyData;
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { authedAdminProcedure, authedProcedure, router } from "../../../trpc";
|
||||||
|
import { ZListLocalInputSchema } from "./listLocal.schema";
|
||||||
|
import { ZQueryForDependenciesInputSchema } from "./queryForDependencies.schema";
|
||||||
|
import { ZSaveKeysInputSchema } from "./saveKeys.schema";
|
||||||
|
import { ZToggleInputSchema } from "./toggle.schema";
|
||||||
|
import { ZUpdateAppCredentialsInputSchema } from "./updateAppCredentials.schema";
|
||||||
|
|
||||||
|
type AppsRouterHandlerCache = {
|
||||||
|
listLocal?: typeof import("./listLocal.handler").listLocalHandler;
|
||||||
|
toggle?: typeof import("./toggle.handler").toggleHandler;
|
||||||
|
saveKeys?: typeof import("./saveKeys.handler").saveKeysHandler;
|
||||||
|
checkForGCal?: typeof import("./checkForGCal.handler").checkForGCalHandler;
|
||||||
|
updateAppCredentials?: typeof import("./updateAppCredentials.handler").updateAppCredentialsHandler;
|
||||||
|
queryForDependencies?: typeof import("./queryForDependencies.handler").queryForDependenciesHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNSTABLE_HANDLER_CACHE: AppsRouterHandlerCache = {};
|
||||||
|
|
||||||
|
export const appsRouter = router({
|
||||||
|
listLocal: authedAdminProcedure.input(ZListLocalInputSchema).query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.listLocal) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.listLocal = await import("./listLocal.handler").then(
|
||||||
|
(mod) => mod.listLocalHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.listLocal) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.listLocal({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
toggle: authedAdminProcedure.input(ZToggleInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.toggle) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.toggle = await import("./toggle.handler").then((mod) => mod.toggleHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.toggle) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.toggle({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
saveKeys: authedAdminProcedure.input(ZSaveKeysInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.saveKeys) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.saveKeys = await import("./saveKeys.handler").then((mod) => mod.saveKeysHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.saveKeys) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.saveKeys({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
checkForGCal: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.checkForGCal) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.checkForGCal = await import("./checkForGCal.handler").then(
|
||||||
|
(mod) => mod.checkForGCalHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.checkForGCal) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.checkForGCal({
|
||||||
|
ctx,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateAppCredentials: authedProcedure
|
||||||
|
.input(ZUpdateAppCredentialsInputSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.updateAppCredentials) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.updateAppCredentials = await import("./updateAppCredentials.handler").then(
|
||||||
|
(mod) => mod.updateAppCredentialsHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.updateAppCredentials) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.updateAppCredentials({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
queryForDependencies: authedProcedure
|
||||||
|
.input(ZQueryForDependenciesInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.queryForDependencies) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.queryForDependencies = await import("./queryForDependencies.handler").then(
|
||||||
|
(mod) => mod.queryForDependenciesHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.queryForDependencies) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.queryForDependencies({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
|
||||||
|
type CheckForGCalOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkForGCalHandler = async ({ ctx }: CheckForGCalOptions) => {
|
||||||
|
const gCalPresent = await prisma.credential.findFirst({
|
||||||
|
where: {
|
||||||
|
type: "google_calendar",
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!gCalPresent;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,89 @@
|
||||||
|
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
import { AppCategories } from "@prisma/client";
|
||||||
|
|
||||||
|
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||||
|
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||||
|
|
||||||
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
import type { TListLocalInputSchema } from "./listLocal.schema";
|
||||||
|
|
||||||
|
type ListLocalOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
prisma: PrismaClient;
|
||||||
|
};
|
||||||
|
input: TListLocalInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listLocalHandler = async ({ ctx, input }: ListLocalOptions) => {
|
||||||
|
const { prisma } = ctx;
|
||||||
|
const category = input.category === "conferencing" ? "video" : input.category;
|
||||||
|
const localApps = getLocalAppMetadata();
|
||||||
|
|
||||||
|
const dbApps = await prisma.app.findMany({
|
||||||
|
where: {
|
||||||
|
categories: {
|
||||||
|
has: AppCategories[category as keyof typeof AppCategories],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
keys: true,
|
||||||
|
enabled: true,
|
||||||
|
dirName: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return localApps.flatMap((app) => {
|
||||||
|
// Filter applications that does not belong to the current requested category.
|
||||||
|
if (!(app.category === category || app.categories?.some((appCategory) => appCategory === category))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find app metadata
|
||||||
|
const dbData = dbApps.find((dbApp) => dbApp.slug === app.slug);
|
||||||
|
|
||||||
|
// If the app already contains keys then return
|
||||||
|
if (dbData?.keys) {
|
||||||
|
return {
|
||||||
|
name: app.name,
|
||||||
|
slug: app.slug,
|
||||||
|
logo: app.logo,
|
||||||
|
title: app.title,
|
||||||
|
type: app.type,
|
||||||
|
description: app.description,
|
||||||
|
// We know that keys are going to be an object or null. Prisma can not type check against JSON fields
|
||||||
|
keys: dbData.keys as Prisma.JsonObject | null,
|
||||||
|
dirName: app.dirName || app.slug,
|
||||||
|
enabled: dbData?.enabled || false,
|
||||||
|
isTemplate: app.isTemplate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysSchema = appKeysSchemas[app.dirName as keyof typeof appKeysSchemas];
|
||||||
|
|
||||||
|
const keys: Record<string, string> = {};
|
||||||
|
|
||||||
|
// `typeof val === 'undefined'` is always slower than !== undefined comparison
|
||||||
|
// it is important to avoid string to string comparisons as much as we can
|
||||||
|
if (keysSchema !== undefined) {
|
||||||
|
// TODO: Remove the Object.values and reduce to improve the performance.
|
||||||
|
Object.values(keysSchema.keyof()._def.values).reduce((keysObject, key) => {
|
||||||
|
keys[key as string] = "";
|
||||||
|
return keysObject;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: app.name,
|
||||||
|
slug: app.slug,
|
||||||
|
logo: app.logo,
|
||||||
|
type: app.type,
|
||||||
|
title: app.title,
|
||||||
|
description: app.description,
|
||||||
|
enabled: dbData?.enabled ?? false,
|
||||||
|
dirName: app.dirName ?? app.slug,
|
||||||
|
keys: Object.keys(keys).length === 0 ? null : keys,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user