2021-10-12 09:35:44 +00:00
|
|
|
import { BookingStatus, Prisma } from "@prisma/client";
|
2021-10-13 11:35:25 +00:00
|
|
|
import _ from "lodash";
|
2021-10-12 09:35:44 +00:00
|
|
|
import { getErrorFromUnknown } from "pages/_error";
|
2021-09-28 08:57:30 +00:00
|
|
|
import { z } from "zod";
|
2021-09-27 14:47:55 +00:00
|
|
|
|
2021-09-28 08:57:30 +00:00
|
|
|
import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername";
|
2021-09-27 14:47:55 +00:00
|
|
|
|
2021-09-28 08:57:30 +00:00
|
|
|
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
|
2021-10-13 11:35:25 +00:00
|
|
|
import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
|
2021-09-28 08:57:30 +00:00
|
|
|
import slugify from "@lib/slugify";
|
2021-09-27 14:47:55 +00:00
|
|
|
|
2021-10-14 19:22:01 +00:00
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
|
2021-10-12 09:35:44 +00:00
|
|
|
import { getCalendarAdapterOrNull } from "../../lib/calendarClient";
|
2021-10-14 10:57:49 +00:00
|
|
|
import { createProtectedRouter, createRouter } from "../createRouter";
|
2021-09-30 20:37:29 +00:00
|
|
|
import { resizeBase64Image } from "../lib/resizeBase64Image";
|
2021-09-28 08:57:30 +00:00
|
|
|
|
|
|
|
const checkUsername =
|
|
|
|
process.env.NEXT_PUBLIC_APP_URL === "https://cal.com" ? checkPremiumUsername : checkRegularUsername;
|
|
|
|
|
2021-10-14 10:57:49 +00:00
|
|
|
// things that unauthenticated users can query about themselves
|
|
|
|
const publicViewerRouter = createRouter()
|
|
|
|
.query("session", {
|
|
|
|
resolve({ ctx }) {
|
|
|
|
return ctx.session;
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.query("i18n", {
|
|
|
|
async resolve({ ctx }) {
|
|
|
|
const { locale, i18n } = ctx;
|
|
|
|
return {
|
|
|
|
i18n,
|
|
|
|
locale,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2021-09-28 08:57:30 +00:00
|
|
|
// routes only available to authenticated users
|
2021-10-14 10:57:49 +00:00
|
|
|
const loggedInViewerRouter = createProtectedRouter()
|
2021-09-27 14:47:55 +00:00
|
|
|
.query("me", {
|
|
|
|
resolve({ ctx }) {
|
2021-10-13 11:35:25 +00:00
|
|
|
const {
|
|
|
|
// pick only the part we want to expose in the API
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
username,
|
|
|
|
email,
|
|
|
|
startTime,
|
|
|
|
endTime,
|
|
|
|
bufferTime,
|
|
|
|
locale,
|
|
|
|
avatar,
|
|
|
|
createdDate,
|
|
|
|
completedOnboarding,
|
2021-10-14 10:57:49 +00:00
|
|
|
twoFactorEnabled,
|
2021-10-13 11:35:25 +00:00
|
|
|
} = ctx.user;
|
|
|
|
const me = {
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
username,
|
|
|
|
email,
|
|
|
|
startTime,
|
|
|
|
endTime,
|
|
|
|
bufferTime,
|
|
|
|
locale,
|
|
|
|
avatar,
|
|
|
|
createdDate,
|
|
|
|
completedOnboarding,
|
2021-10-14 10:57:49 +00:00
|
|
|
twoFactorEnabled,
|
2021-10-13 11:35:25 +00:00
|
|
|
};
|
|
|
|
return me;
|
2021-09-27 14:47:55 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
.query("bookings", {
|
2021-09-30 10:46:39 +00:00
|
|
|
input: z.object({
|
2021-10-02 13:29:26 +00:00
|
|
|
status: z.enum(["upcoming", "past", "cancelled"]),
|
2021-09-30 10:46:39 +00:00
|
|
|
}),
|
|
|
|
async resolve({ ctx, input }) {
|
2021-09-27 14:47:55 +00:00
|
|
|
const { prisma, user } = ctx;
|
2021-10-02 13:29:26 +00:00
|
|
|
const bookingListingByStatus = input.status;
|
2021-09-30 10:46:39 +00:00
|
|
|
const bookingListingFilters: Record<typeof bookingListingByStatus, Prisma.BookingWhereInput[]> = {
|
2021-10-08 15:58:37 +00:00
|
|
|
upcoming: [{ endTime: { gte: new Date() }, NOT: { status: { equals: BookingStatus.CANCELLED } } }],
|
|
|
|
past: [{ endTime: { lte: new Date() }, NOT: { status: { equals: BookingStatus.CANCELLED } } }],
|
2021-09-30 10:46:39 +00:00
|
|
|
cancelled: [{ status: { equals: BookingStatus.CANCELLED } }],
|
|
|
|
};
|
|
|
|
const bookingListingOrderby: Record<typeof bookingListingByStatus, Prisma.BookingOrderByInput> = {
|
|
|
|
upcoming: { startTime: "desc" },
|
|
|
|
past: { startTime: "asc" },
|
|
|
|
cancelled: { startTime: "asc" },
|
|
|
|
};
|
|
|
|
const passedBookingsFilter = bookingListingFilters[bookingListingByStatus];
|
|
|
|
const orderBy = bookingListingOrderby[bookingListingByStatus];
|
|
|
|
|
2021-09-27 14:47:55 +00:00
|
|
|
const bookingsQuery = await prisma.booking.findMany({
|
|
|
|
where: {
|
|
|
|
OR: [
|
|
|
|
{
|
|
|
|
userId: user.id,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
attendees: {
|
|
|
|
some: {
|
|
|
|
email: user.email,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2021-09-30 10:46:39 +00:00
|
|
|
AND: passedBookingsFilter,
|
2021-09-27 14:47:55 +00:00
|
|
|
},
|
|
|
|
select: {
|
|
|
|
uid: true,
|
|
|
|
title: true,
|
|
|
|
description: true,
|
|
|
|
attendees: true,
|
|
|
|
confirmed: true,
|
|
|
|
rejected: true,
|
|
|
|
id: true,
|
|
|
|
startTime: true,
|
|
|
|
endTime: true,
|
|
|
|
eventType: {
|
|
|
|
select: {
|
|
|
|
team: {
|
|
|
|
select: {
|
|
|
|
name: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
status: true,
|
|
|
|
},
|
2021-09-30 10:46:39 +00:00
|
|
|
orderBy,
|
2021-09-27 14:47:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const bookings = bookingsQuery.reverse().map((booking) => {
|
|
|
|
return {
|
|
|
|
...booking,
|
|
|
|
startTime: booking.startTime.toISOString(),
|
|
|
|
endTime: booking.endTime.toISOString(),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
return bookings;
|
|
|
|
},
|
2021-09-28 08:57:30 +00:00
|
|
|
})
|
2021-10-12 09:35:44 +00:00
|
|
|
.query("integrations", {
|
|
|
|
async resolve({ ctx }) {
|
|
|
|
const { user } = ctx;
|
|
|
|
const { credentials } = user;
|
|
|
|
|
2021-10-13 11:35:25 +00:00
|
|
|
function countActive(items: { credentialIds: unknown[] }[]) {
|
|
|
|
return items.reduce((acc, item) => acc + item.credentialIds.length, 0);
|
2021-10-12 09:35:44 +00:00
|
|
|
}
|
2021-10-13 11:35:25 +00:00
|
|
|
const integrations = ALL_INTEGRATIONS.map((integration) => ({
|
|
|
|
...integration,
|
|
|
|
credentialIds: credentials
|
|
|
|
.filter((credential) => credential.type === integration.type)
|
|
|
|
.map((credential) => credential.id),
|
|
|
|
}));
|
|
|
|
// `flatMap()` these work like `.filter()` but infers the types correctly
|
2021-10-12 09:35:44 +00:00
|
|
|
const conferencing = integrations.flatMap((item) => (item.variant === "conferencing" ? [item] : []));
|
|
|
|
const payment = integrations.flatMap((item) => (item.variant === "payment" ? [item] : []));
|
|
|
|
const calendar = integrations.flatMap((item) => (item.variant === "calendar" ? [item] : []));
|
|
|
|
|
|
|
|
// get user's credentials + their connected integrations
|
|
|
|
const calendarCredentials = user.credentials
|
|
|
|
.filter((credential) => credential.type.endsWith("_calendar"))
|
|
|
|
.flatMap((credential) => {
|
|
|
|
const integration = ALL_INTEGRATIONS.find((integration) => integration.type === credential.type);
|
|
|
|
|
|
|
|
const adapter = getCalendarAdapterOrNull({
|
|
|
|
...credential,
|
|
|
|
userId: user.id,
|
|
|
|
});
|
|
|
|
return integration && adapter && integration.variant === "calendar"
|
|
|
|
? [{ integration, credential, adapter }]
|
|
|
|
: [];
|
|
|
|
});
|
|
|
|
|
|
|
|
// get all the connected integrations' calendars (from third party)
|
|
|
|
const connectedCalendars = await Promise.all(
|
|
|
|
calendarCredentials.map(async (item) => {
|
|
|
|
const { adapter, integration, credential } = item;
|
2021-10-13 11:35:25 +00:00
|
|
|
|
|
|
|
const credentialId = credential.id;
|
2021-10-12 09:35:44 +00:00
|
|
|
try {
|
2021-10-13 11:35:25 +00:00
|
|
|
const cals = await adapter.listCalendars();
|
|
|
|
const calendars = _(cals)
|
|
|
|
.map((cal) => ({
|
|
|
|
...cal,
|
|
|
|
isSelected: user.selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
|
|
|
}))
|
|
|
|
.sortBy(["primary"])
|
|
|
|
.value();
|
2021-10-12 09:35:44 +00:00
|
|
|
const primary = calendars.find((item) => item.primary) ?? calendars[0];
|
|
|
|
if (!primary) {
|
2021-10-13 11:35:25 +00:00
|
|
|
throw new Error("No primary calendar found");
|
2021-10-12 09:35:44 +00:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
integration,
|
2021-10-13 11:35:25 +00:00
|
|
|
credentialId,
|
2021-10-12 09:35:44 +00:00
|
|
|
primary,
|
|
|
|
calendars,
|
|
|
|
};
|
|
|
|
} catch (_error) {
|
|
|
|
const error = getErrorFromUnknown(_error);
|
|
|
|
return {
|
|
|
|
integration,
|
2021-10-13 11:35:25 +00:00
|
|
|
credentialId,
|
2021-10-12 09:35:44 +00:00
|
|
|
error: {
|
|
|
|
message: error.message,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
conferencing: {
|
|
|
|
items: conferencing,
|
|
|
|
numActive: countActive(conferencing),
|
|
|
|
},
|
|
|
|
calendar: {
|
|
|
|
items: calendar,
|
|
|
|
numActive: countActive(calendar),
|
|
|
|
},
|
|
|
|
payment: {
|
|
|
|
items: payment,
|
|
|
|
numActive: countActive(payment),
|
|
|
|
},
|
|
|
|
connectedCalendars,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
})
|
2021-09-28 08:57:30 +00:00
|
|
|
.mutation("updateProfile", {
|
|
|
|
input: z.object({
|
|
|
|
username: z.string().optional(),
|
|
|
|
name: z.string().optional(),
|
|
|
|
bio: z.string().optional(),
|
|
|
|
avatar: z.string().optional(),
|
|
|
|
timeZone: z.string().optional(),
|
|
|
|
weekStart: z.string().optional(),
|
|
|
|
hideBranding: z.boolean().optional(),
|
2021-10-02 20:16:51 +00:00
|
|
|
theme: z.string().optional().nullable(),
|
2021-09-28 08:57:30 +00:00
|
|
|
completedOnboarding: z.boolean().optional(),
|
|
|
|
locale: z.string().optional(),
|
|
|
|
}),
|
|
|
|
async resolve({ input, ctx }) {
|
|
|
|
const { user, prisma } = ctx;
|
|
|
|
const data: Prisma.UserUpdateInput = {
|
|
|
|
...input,
|
|
|
|
};
|
|
|
|
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);
|
|
|
|
if (!response.available) {
|
|
|
|
throw new TRPCError({ code: "BAD_REQUEST", message: response.message });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-09-30 20:37:29 +00:00
|
|
|
if (input.avatar) {
|
|
|
|
data.avatar = await resizeBase64Image(input.avatar);
|
|
|
|
}
|
2021-09-28 08:57:30 +00:00
|
|
|
|
|
|
|
await prisma.user.update({
|
|
|
|
where: {
|
|
|
|
id: user.id,
|
|
|
|
},
|
|
|
|
data,
|
|
|
|
});
|
|
|
|
},
|
2021-09-27 14:47:55 +00:00
|
|
|
});
|
2021-10-14 10:57:49 +00:00
|
|
|
|
|
|
|
export const viewerRouter = createRouter().merge(publicViewerRouter).merge(loggedInViewerRouter);
|