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>pull/8479/head^2
parent
a4725920ff
commit
1eeb91a793
|
@ -6,6 +6,6 @@
|
|||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"spellright.language": ["en"],
|
||||
"spellright.documentTypes": ["markdown", "typescript"],
|
||||
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
|
||||
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
||||
}
|
||||
|
|
|
@ -71,5 +71,5 @@ module.exports = {
|
|||
|
||||
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 { nameOfDay } from "@calcom/lib/weekday";
|
||||
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 classNames from "@lib/classNames";
|
||||
|
|
|
@ -483,12 +483,12 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||
i18n: { language },
|
||||
} = useLocale();
|
||||
const now = new Date();
|
||||
const recurringCount = recurringDates.filter((date) => {
|
||||
const recurringCount = recurringDates.filter((recurringDate) => {
|
||||
return (
|
||||
date >= now &&
|
||||
recurringDate >= now &&
|
||||
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||
.map((date) => date.toDateString())
|
||||
.includes(date.toDateString())
|
||||
.includes(recurringDate.toDateString())
|
||||
);
|
||||
}).length;
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||
import prisma from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
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 {
|
||||
ACCEPT = "accept",
|
||||
|
@ -51,7 +51,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
|||
try {
|
||||
/** @see https://trpc.io/docs/server-side-calls */
|
||||
const ctx = await createContext({ req, res }, sessionGetter);
|
||||
const caller = viewerRouter.createCaller(ctx);
|
||||
const caller = viewerRouter.createCaller({ ...ctx, req, res });
|
||||
|
||||
await caller.bookings.confirm({
|
||||
bookingId: booking.id,
|
||||
recurringEventId: booking.recurringEventId || undefined,
|
||||
|
|
|
@ -15,15 +15,14 @@ import { v4 as uuidv4 } from "uuid";
|
|||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { BookingStatus } from "@calcom/prisma/client";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||
import { getSchedule } 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/util";
|
||||
|
||||
import { prismaMock, CalendarManagerMock } from "../../../../tests/config/singleton";
|
||||
|
||||
// TODO: Mock properly
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
||||
prismaMock.user.findMany.mockResolvedValue([]);
|
||||
prismaMock.selectedSlots.findMany.mockResolvedValue([]);
|
||||
|
||||
jest.mock("@calcom/lib/constants", () => ({
|
||||
IS_PRODUCTION: true,
|
||||
|
@ -271,16 +270,13 @@ describe("getSchedule", () => {
|
|||
end: `${plus2DateString}T23:00:00.000Z`,
|
||||
},
|
||||
]);
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
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
|
||||
const scheduleOnCompletelyFreeDay = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
// EventTypeSlug doesn't matter for non-dynamic events
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleOnCompletelyFreeDay = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
// EventTypeSlug doesn't matter for non-dynamic events
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
// getSchedule returns timeslots in GMT
|
||||
expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots(
|
||||
|
@ -390,16 +383,13 @@ describe("getSchedule", () => {
|
|||
);
|
||||
|
||||
// Day plus 3
|
||||
const scheduleForDayWithOneBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForDayWithOneBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -455,16 +445,13 @@ describe("getSchedule", () => {
|
|||
});
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const scheduleForEventWith30Length = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForEventWith30Length = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
||||
[
|
||||
`04:00:00.000Z`,
|
||||
|
@ -490,16 +477,13 @@ describe("getSchedule", () => {
|
|||
}
|
||||
);
|
||||
|
||||
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule(
|
||||
{
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
// `slotInterval` takes precedence over `length`
|
||||
expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots(
|
||||
[`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: minus1DateString } = getDate({ dateIncrement: -1 });
|
||||
const scheduleForEventWithBookingNotice13Hrs = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
||||
[
|
||||
/*`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(
|
||||
{
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
||||
[
|
||||
/*`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(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -714,16 +689,13 @@ describe("getSchedule", () => {
|
|||
},
|
||||
]);
|
||||
|
||||
const scheduleForEventOnADayWithCalBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForEventOnADayWithCalBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -767,16 +739,13 @@ describe("getSchedule", () => {
|
|||
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForEventOnADayWithDateOverride = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForEventOnADayWithDateOverride = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
||||
["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
|
||||
// individual Event Type
|
||||
const thisUserAvailability = await getSchedule(
|
||||
{
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const thisUserAvailability = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
expect(thisUserAvailability).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -951,16 +917,13 @@ describe("getSchedule", () => {
|
|||
hosts: [],
|
||||
});
|
||||
|
||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -981,16 +944,13 @@ describe("getSchedule", () => {
|
|||
}
|
||||
);
|
||||
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
// 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
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUser).toHaveTimeSlots(
|
||||
|
@ -1088,16 +1048,13 @@ describe("getSchedule", () => {
|
|||
],
|
||||
hosts: [],
|
||||
});
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -1116,16 +1073,13 @@ describe("getSchedule", () => {
|
|||
{ dateString: plus2DateString }
|
||||
);
|
||||
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
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 abi from "../utils/abi.json";
|
||||
import { checkBalance, getProviders, SUPPORTED_CHAINS } from "../utils/ethereum";
|
||||
import { ZBalanceInputSchema, ZBalanceOutputSchema } from "./balance.schema";
|
||||
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({
|
||||
// Fetch contract `name` and `symbol` or error
|
||||
contract: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
chainId: z.number(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
data: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
symbol: z.string(),
|
||||
})
|
||||
.nullish(),
|
||||
error: z.string().nullish(),
|
||||
})
|
||||
)
|
||||
.input(ZContractInputSchema)
|
||||
.output(ZContractOutputSchema)
|
||||
.query(async ({ input }) => {
|
||||
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",
|
||||
},
|
||||
};
|
||||
if (!UNSTABLE_HANDLER_CACHE.contract) {
|
||||
UNSTABLE_HANDLER_CACHE.contract = await import("./contract.handler").then(
|
||||
(mod) => mod.contractHandler
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
balance: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
tokenAddress: z.string(),
|
||||
chainId: z.number(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
data: z
|
||||
.object({
|
||||
hasBalance: z.boolean(),
|
||||
})
|
||||
.nullish(),
|
||||
error: z.string().nullish(),
|
||||
})
|
||||
)
|
||||
.input(ZBalanceInputSchema)
|
||||
.output(ZBalanceOutputSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { address, tokenAddress, chainId } = input;
|
||||
try {
|
||||
const hasBalance = await checkBalance(address, tokenAddress, chainId);
|
||||
|
||||
return {
|
||||
data: {
|
||||
hasBalance,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
data: {
|
||||
hasBalance: false,
|
||||
},
|
||||
};
|
||||
if (!UNSTABLE_HANDLER_CACHE.balance) {
|
||||
UNSTABLE_HANDLER_CACHE.balance = await import("./balance.handler").then((mod) => mod.balanceHandler);
|
||||
}
|
||||
|
||||
// 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"] = {
|
||||
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 { HorizontalTabs } from "@calcom/ui";
|
||||
import { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
|
||||
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
|
||||
|
||||
import { FiltersContainer } from "../components/FiltersContainer";
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
|
|||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||
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";
|
||||
|
||||
export const config = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import crypto from "crypto";
|
||||
import md5 from "md5";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
@ -715,7 +715,7 @@ export const insightsRouter = router({
|
|||
return {
|
||||
userId: booking.userId,
|
||||
user: userHashMap.get(booking.userId),
|
||||
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
|
||||
emailMd5: md5(user?.email),
|
||||
count: booking._count.id,
|
||||
};
|
||||
});
|
||||
|
@ -806,7 +806,7 @@ export const insightsRouter = router({
|
|||
return {
|
||||
userId: booking.userId,
|
||||
user: userHashMap.get(booking.userId),
|
||||
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
|
||||
emailMd5: md5(user?.email),
|
||||
count: booking._count.id,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export { useSchedule } from "./useSchedule";
|
||||
export { useSlotsForDate } from "./useSlotsForDate";
|
||||
export { useNonEmptyScheduleDays } from "./useNonEmptyScheduleDays";
|
||||
export type { Slots } from "./types";
|
||||
export type { Slots } from "./types";
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -7,7 +7,8 @@ import { trpc } from "@calcom/trpc/react";
|
|||
import { Meta, showToast, SkeletonContainer } from "@calcom/ui";
|
||||
|
||||
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() });
|
||||
|
||||
|
|
|
@ -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 superjson from "superjson";
|
||||
|
||||
import { httpBatchLink } from "../client/links/httpBatchLink";
|
||||
import { httpLink } from "../client/links/httpLink";
|
||||
import { loggerLink } from "../client/links/loggerLink";
|
||||
import { splitLink } from "../client/links/splitLink";
|
||||
|
@ -8,7 +9,6 @@ import { createTRPCNext } from "../next";
|
|||
// ℹ️ Type-only import:
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
||||
import type { TRPCClientErrorLike } from "../react";
|
||||
import { httpBatchLink } from "../react";
|
||||
import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server";
|
||||
import type { AppRouter } from "../server/routers/_app";
|
||||
|
||||
|
|
|
@ -77,3 +77,5 @@ export const createContext = async (
|
|||
res,
|
||||
};
|
||||
};
|
||||
|
||||
export type TRPCContext = Awaited<ReturnType<typeof createContext>>;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* This file contains the root router of your tRPC-backend
|
||||
*/
|
||||
import { router } from "../trpc";
|
||||
import { viewerRouter } from "./viewer";
|
||||
import { viewerRouter } from "./viewer/_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 {};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue