2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1 @@
export * from "@trpc/client";

View File

@@ -0,0 +1,4 @@
export * from "./client";
export * from "./next";
export * from "./react";
export * from "./server";

View File

@@ -0,0 +1 @@
export * from "@trpc/next";

View File

@@ -0,0 +1,22 @@
{
"name": "@calcom/trpc",
"sideEffects": false,
"private": true,
"description": "Shared tRPC library for Cal.com",
"authors": "zomars",
"version": "1.0.0",
"main": "index.ts",
"scripts": {
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix"
},
"dependencies": {
"@tanstack/react-query": "^5.17.15",
"@trpc/client": "11.0.0-next-beta.222",
"@trpc/next": "11.0.0-next-beta.222",
"@trpc/react-query": "11.0.0-next-beta.222",
"@trpc/server": "11.0.0-next-beta.222",
"superjson": "1.9.1",
"zod": "^3.22.4"
}
}

View File

@@ -0,0 +1,13 @@
import { trpc } from "../trpc";
export function useEmailVerifyCheck() {
const emailCheck = trpc.viewer.shouldVerifyEmail.useQuery(undefined, {
retry(failureCount) {
return failureCount > 3;
},
});
return emailCheck;
}
export default useEmailVerifyCheck;

View File

@@ -0,0 +1,13 @@
import { trpc } from "../trpc";
export function useMeQuery() {
const meQuery = trpc.viewer.me.useQuery(undefined, {
retry(failureCount) {
return failureCount > 3;
},
});
return meQuery;
}
export default useMeQuery;

View File

@@ -0,0 +1,2 @@
export * from "@trpc/react-query";
export * from "./trpc";

View File

@@ -0,0 +1 @@
export * from "@trpc/react-query/server";

View File

@@ -0,0 +1,31 @@
export * from "@trpc/react-query/shared";
export const ENDPOINTS = [
"admin",
"apiKeys",
"appRoutingForms",
"apps",
"auth",
"availability",
"appBasecamp3",
"bookings",
"deploymentSetup",
"dsync",
"eventTypes",
"features",
"insights",
"payments",
"public",
"timezones",
"saml",
"slots",
"teams",
"organizations",
"users",
"viewer",
"webhook",
"workflows",
"appsRouter",
"googleWorkspace",
"oAuth",
] as const;

View File

@@ -0,0 +1,139 @@
import type { NextPageContext } from "next/types";
import superjson from "superjson";
import { httpBatchLink } from "../client";
import { httpLink } from "../client";
import { loggerLink } from "../client";
import { splitLink } from "../client";
import type { CreateTRPCNext } from "../next";
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 type { inferRouterInputs, inferRouterOutputs } from "../server";
import type { AppRouter } from "../server/routers/_app";
import { ENDPOINTS } from "./shared";
type Maybe<T> = T | null | undefined;
/**
* We deploy our tRPC router on multiple lambdas to keep number of imports as small as possible
* TODO: Make this dynamic based on folders in trpc server?
*/
export type Endpoint = (typeof ENDPOINTS)[number];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolveEndpoint = (links: any) => {
// TODO: Update our trpc routes so they are more clear.
// This function parses paths like the following and maps them
// to the correct API endpoints.
// - viewer.me - 2 segment paths like this are for logged in requests
// - viewer.public.i18n - 3 segments paths can be public or authed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ctx: any) => {
const parts = ctx.op.path.split(".");
let endpoint;
let path = "";
if (parts.length == 2) {
endpoint = parts[0] as keyof typeof links;
path = parts[1];
} else {
endpoint = parts[1] as keyof typeof links;
path = parts.splice(2, parts.length - 2).join(".");
}
return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
};
};
/**
* A set of strongly-typed React hooks from your `AppRouter` type signature with `createTRPCReact`.
* @link https://trpc.io/docs/v10/react#2-create-trpc-hooks
*/
export const trpc: CreateTRPCNext<AppRouter, NextPageContext, null> = createTRPCNext<
AppRouter,
NextPageContext
>({
config() {
const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
/**
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
*/
return {
/**
* @link https://trpc.io/docs/links
*/
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
}),
splitLink({
// check for context property `skipBatch`
condition: (op) => !!op.context.skipBatch,
// when condition is true, use normal request
true: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [endpoint, httpLink({ url: `${url}/${endpoint}` })(runtime)])
);
return resolveEndpoint(links);
},
// when condition is false, use batch request
false: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [endpoint, httpBatchLink({ url: `${url}/${endpoint}` })(runtime)])
);
return resolveEndpoint(links);
},
}),
],
/**
* @link https://react-query.tanstack.com/reference/QueryClient
*/
queryClientConfig: {
defaultOptions: {
queries: {
/**
* 1s should be enough to just keep identical query waterfalls low
* @example if one page components uses a query that is also used further down the tree
*/
staleTime: 1000,
/**
* Retry `useQuery()` calls depending on this function
*/
retry(failureCount, _err) {
const err = _err as never as Maybe<TRPCClientErrorLike<AppRouter>>;
const code = err?.data?.code;
if (code === "BAD_REQUEST" || code === "FORBIDDEN" || code === "UNAUTHORIZED") {
// if input data is wrong or you're not authorized there's no point retrying a query
return false;
}
const MAX_QUERY_RETRIES = 3;
return failureCount < MAX_QUERY_RETRIES;
},
},
},
},
/**
* @link https://trpc.io/docs/data-transformers
*/
transformer: superjson,
};
},
/**
* @link https://trpc.io/docs/ssr
*/
ssr: false,
});
export const transformer = superjson;
export type RouterInputs = inferRouterInputs<AppRouter>;
export type RouterOutputs = inferRouterOutputs<AppRouter>;

View File

@@ -0,0 +1 @@
export * from "@trpc/server/adapters/next";

View File

@@ -0,0 +1,104 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import type { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import getIP from "@calcom/lib/getIP";
import prisma, { readonlyPrisma } from "@calcom/prisma";
import type { SelectedCalendar, User as PrismaUser } from "@calcom/prisma/client";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
type CreateContextOptions =
| (Omit<CreateNextContextOptions, "info"> & {
info?: CreateNextContextOptions["info"];
})
| GetServerSidePropsContext;
export type CreateInnerContextOptions = {
sourceIp?: string;
session?: Session | null;
locale: string;
user?:
| Omit<
PrismaUser,
| "locale"
| "twoFactorSecret"
| "emailVerified"
| "password"
| "identityProviderId"
| "invitedTo"
| "allowDynamicBooking"
| "verified"
> & {
locale: Exclude<PrismaUser["locale"], null>;
credentials?: Credential[];
selectedCalendars?: Partial<SelectedCalendar>[];
rawAvatar?: string;
};
i18n?: Awaited<ReturnType<typeof serverSideTranslations>>;
} & Partial<CreateContextOptions>;
export type GetSessionFn =
| ((_options: {
req: GetServerSidePropsContext["req"] | NextApiRequest;
res: GetServerSidePropsContext["res"] | NextApiResponse;
}) => Promise<Session | null>)
| (() => Promise<Session | null>);
export type InnerContext = CreateInnerContextOptions & {
prisma: typeof prisma;
insightsDb: typeof readonlyPrisma;
};
/**
* Inner context. Will always be available in your procedures, in contrast to the outer context.
*
* Also useful for:
* - testing, so you don't have to mock Next.js' `req`/`res`
* - tRPC's `createServerSideHelpers` where we don't have `req`/`res`
*
* @see https://trpc.io/docs/context#inner-and-outer-context
*/
export async function createContextInner(opts: CreateInnerContextOptions): Promise<InnerContext> {
return {
prisma,
insightsDb: readonlyPrisma,
...opts,
};
}
type Context = InnerContext & {
req: CreateContextOptions["req"];
res: CreateContextOptions["res"];
};
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export const createContext = async (
{ req, res }: CreateContextOptions,
sessionGetter?: GetSessionFn
): Promise<Context> => {
const locale = await getLocale(req);
// This type may not be accurate if this request is coming from SSG init but they both should satisfy the requirements of getIP.
// TODO: @sean - figure out a way to make getIP be happy with trpc req. params
const sourceIp = getIP(req as NextApiRequest);
const session = !!sessionGetter ? await sessionGetter({ req, res }) : null;
const contextInner = await createContextInner({ locale, session, sourceIp });
return {
...contextInner,
req,
res,
};
};
export type TRPCContext = Awaited<ReturnType<typeof createContext>>;
export type TRPCContextInner = Awaited<ReturnType<typeof createContextInner>>;
export type WithLocale<T extends TRPCContext = any> = T &
Required<Pick<CreateInnerContextOptions, "i18n" | "locale">>;
export type WithSession<T extends TRPCContext = any> = T &
Required<Pick<CreateInnerContextOptions, "session">>;

View File

@@ -0,0 +1,96 @@
import { z } from "zod";
import type { AnyRouter } from "@trpc/server";
import { createNextApiHandler as _createNextApiHandler } from "@trpc/server/adapters/next";
import { createContext as createTrpcContext } from "./createContext";
/**
* Creates an API handler executed by Next.js.
*/
export function createNextApiHandler(router: AnyRouter, isPublic = false, namespace = "") {
return _createNextApiHandler({
router,
/**
* @link https://trpc.io/docs/context
*/
createContext: (opts) => {
return createTrpcContext(opts);
},
/**
* @link https://trpc.io/docs/error-handling
*/
onError({ error }) {
if (error.code === "INTERNAL_SERVER_ERROR") {
// send to bug reporting
console.error("Something went wrong", error);
}
},
/**
* Enable query batching
*/
batching: {
enabled: true,
},
/**
* @link https://trpc.io/docs/caching#api-response-caching
*/
responseMeta({ ctx, paths, type, errors }) {
const allOk = errors.length === 0;
const isQuery = type === "query";
const noHeaders = {};
// We cannot set headers on SSG queries
if (!ctx?.res) return noHeaders;
const defaultHeaders: Record<"headers", Record<string, string>> = {
headers: {},
};
const timezone = z.string().safeParse(ctx.req?.headers["x-vercel-ip-timezone"]);
if (timezone.success) defaultHeaders.headers["x-cal-timezone"] = timezone.data;
// We need all these conditions to be true to set cache headers
if (!(isPublic && allOk && isQuery)) return defaultHeaders;
// No cache by default
defaultHeaders.headers["cache-control"] = `no-cache`;
if (isPublic && paths) {
const FIVE_MINUTES_IN_SECONDS = 5 * 60;
const ONE_YEAR_IN_SECONDS = 31536000;
const SETTING_FOR_CACHED_BY_VERSION =
process.env.NODE_ENV === "development" ? "no-cache" : `max-age=${ONE_YEAR_IN_SECONDS}`;
const cacheRules = {
session: "no-cache",
// i18n and cityTimezones are now being accessed using the CalComVersion, which updates on every release,
// letting the clients get the new versions when the version number changes.
i18n: SETTING_FOR_CACHED_BY_VERSION,
cityTimezones: SETTING_FOR_CACHED_BY_VERSION,
// FIXME: Using `max-age=1, stale-while-revalidate=60` fails some booking tests.
"slots.getSchedule": `no-cache`,
// Feature Flags change but it might be okay to have a 5 minute cache to avoid burdening the servers with requests for this.
// Note that feature flags can be used to quickly kill a feature if it's not working as expected. So, we have to keep fresh time lesser than the deployment time atleast
"features.map": `max-age=${FIVE_MINUTES_IN_SECONDS}, stale-while-revalidate=60`, // "map" - Feature Flag Map
} as const;
const prependNamespace = (key: string) =>
(namespace ? `${namespace}.${key}` : key) as keyof typeof cacheRules;
const matchedPath = paths.find((v) => prependNamespace(v) in cacheRules);
if (matchedPath) {
const cacheRule = cacheRules[prependNamespace(matchedPath)];
// We must set cdn-cache-control as well to ensure that Vercel doesn't strip stale-while-revalidate
// https://vercel.com/docs/concepts/edge-network/caching#:~:text=If%20you%20set,in%20the%20response.
defaultHeaders.headers["cache-control"] = defaultHeaders.headers["cdn-cache-control"] = cacheRule;
}
}
return defaultHeaders;
},
});
}

View File

@@ -0,0 +1 @@
export * from "@trpc/server";

View File

@@ -0,0 +1,20 @@
import { captureException as SentryCaptureException } from "@sentry/nextjs";
import { redactError } from "@calcom/lib/redactError";
import { middleware } from "../trpc";
const captureErrorsMiddleware = middleware(async ({ next }) => {
const result = await next();
if (result && !result.ok) {
const cause = result.error.cause;
if (!cause) {
return result;
}
SentryCaptureException(cause);
throw redactError(cause);
}
return result;
});
export default captureErrorsMiddleware;

View File

@@ -0,0 +1,11 @@
import { middleware } from "../trpc";
const perfMiddleware = middleware(async ({ path, type, next }) => {
performance.mark("Start");
const result = await next();
performance.mark("End");
performance.measure(`[${result.ok ? "OK" : "ERROR"}][$1] ${type} '${path}'`, "Start", "End");
return result;
});
export default perfMiddleware;

View File

@@ -0,0 +1,235 @@
import type { Session } from "next-auth";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
import type { TRPCContextInner } from "../createContext";
import { middleware } from "../trpc";
type Maybe<T> = T | null | undefined;
export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<Session>) {
const { prisma } = ctx;
if (!session) {
return null;
}
if (!session.user?.id) {
return null;
}
const userFromDb = await prisma.user.findUnique({
where: {
id: session.user.id,
// Locked users can't login
locked: false,
},
select: {
id: true,
username: true,
name: true,
email: true,
emailVerified: true,
bio: true,
avatarUrl: true,
timeZone: true,
weekStart: true,
startTime: true,
endTime: true,
defaultScheduleId: true,
bufferTime: true,
theme: true,
appTheme: true,
createdDate: true,
hideBranding: true,
twoFactorEnabled: true,
disableImpersonation: true,
identityProvider: true,
identityProviderId: true,
brandColor: true,
darkBrandColor: true,
movedToProfileId: true,
selectedCalendars: {
select: {
externalId: true,
integration: true,
},
},
completedOnboarding: true,
destinationCalendar: true,
locale: true,
timeFormat: true,
trialEndsAt: true,
metadata: true,
role: true,
allowDynamicBooking: true,
allowSEOIndexing: true,
receiveMonthlyDigestEmail: true,
},
});
// some hacks to make sure `username` and `email` are never inferred as `null`
if (!userFromDb) {
return null;
}
const upId = session.upId;
const user = await UserRepository.enrichUserWithTheProfile({
user: userFromDb,
upId,
});
logger.debug(
`getUserFromSession: enriched user with profile - ${ctx.req?.url}`,
safeStringify({ user, userFromDb, upId })
);
const { email, username, id } = user;
if (!email || !id) {
return null;
}
const userMetaData = userMetadata.parse(user.metadata || {});
const orgMetadata = teamMetadataSchema.parse(user.profile?.organization?.metadata || {});
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
const locale = user?.locale ?? ctx.locale;
const isOrgAdmin = !!user.profile?.organization?.members.filter(
(member) => (member.role === "ADMIN" || member.role === "OWNER") && member.userId === user.id
).length;
if (isOrgAdmin) {
logger.debug("User is an org admin", safeStringify({ userId: user.id }));
} else {
logger.debug("User is not an org admin", safeStringify({ userId: user.id }));
}
// Want to reduce the amount of data being sent
if (isOrgAdmin && user.profile?.organization?.members) {
user.profile.organization.members = [];
}
const organization = {
...user.profile?.organization,
id: user.profile?.organization?.id ?? null,
isOrgAdmin,
metadata: orgMetadata,
requestedSlug: orgMetadata?.requestedSlug ?? null,
};
return {
...user,
avatar: `${WEBAPP_URL}/${user.username}/avatar.png?${organization.id}` && `orgId=${organization.id}`,
// TODO: OrgNewSchema - later - We could consolidate the props in user.profile?.organization as organization is a profile thing now.
organization,
organizationId: organization.id,
id,
email,
username,
locale,
defaultBookerLayouts: userMetaData?.defaultBookerLayouts || null,
};
}
export type UserFromSession = Awaited<ReturnType<typeof getUserFromSession>>;
const getSession = async (ctx: TRPCContextInner) => {
const { req, res } = ctx;
const { getServerSession } = await import("@calcom/features/auth/lib/getServerSession");
return req ? await getServerSession({ req, res }) : null;
};
const getUserSession = async (ctx: TRPCContextInner) => {
/**
* It is possible that the session and user have already been added to the context by a previous middleware
* or when creating the context
*/
const session = ctx.session || (await getSession(ctx));
const user = session ? await getUserFromSession(ctx, session) : null;
let foundProfile = null;
// Check authorization for profile
if (session?.profileId && user?.id) {
foundProfile = await ProfileRepository.findByUserIdAndProfileId({
userId: user.id,
profileId: session.profileId,
});
if (!foundProfile) {
logger.error(
"Profile not found or not authorized",
safeStringify({ profileId: session.profileId, userId: user?.id })
);
// TODO: Test that logout should happen automatically
throw new TRPCError({ code: "UNAUTHORIZED", message: "Profile not found or not authorized" });
}
}
let sessionWithUpId = null;
if (session) {
let upId = session.upId;
if (!upId) {
upId = foundProfile?.upId ?? `usr-${user?.id}`;
}
if (!upId) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "No upId found for session" });
}
sessionWithUpId = {
...session,
upId,
};
}
return { user, session: sessionWithUpId };
};
const sessionMiddleware = middleware(async ({ ctx, next }) => {
const middlewareStart = performance.now();
const { user, session } = await getUserSession(ctx);
const middlewareEnd = performance.now();
logger.debug("Perf:t.sessionMiddleware", middlewareEnd - middlewareStart);
return next({
ctx: { user, session },
});
});
export const isAuthed = middleware(async ({ ctx, next }) => {
const middlewareStart = performance.now();
const { user, session } = await getUserSession(ctx);
const middlewareEnd = performance.now();
logger.debug("Perf:t.isAuthed", middlewareEnd - middlewareStart);
if (!user || !session) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: { user, session },
});
});
export const isAdminMiddleware = isAuthed.unstable_pipe(({ ctx, next }) => {
const { user } = ctx;
if (user?.role !== "ADMIN") {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { user: user } });
});
// Org admins can be admins or owners
export const isOrgAdminMiddleware = isAuthed.unstable_pipe(({ ctx, next }) => {
const { user } = ctx;
if (!user?.organization?.isOrgAdmin) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { user: user } });
});
export default sessionMiddleware;

View File

@@ -0,0 +1,33 @@
import captureErrorsMiddleware from "../middlewares/captureErrorsMiddleware";
import perfMiddleware from "../middlewares/perfMiddleware";
import { isAdminMiddleware, isAuthed, isOrgAdminMiddleware } from "../middlewares/sessionMiddleware";
import { procedure } from "../trpc";
import publicProcedure from "./publicProcedure";
/*interface IRateLimitOptions {
intervalInMs: number;
limit: number;
}
const isRateLimitedByUserIdMiddleware = ({ intervalInMs, limit }: IRateLimitOptions) =>
middleware(({ ctx, next }) => {
// validate user exists
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const { isRateLimited } = rateLimit({ intervalInMs }).check(limit, ctx.user.id.toString());
if (isRateLimited) {
throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
}
return next({ ctx: { user: ctx.user, session: ctx.session } });
});
*/
const authedProcedure = procedure.use(captureErrorsMiddleware).use(perfMiddleware).use(isAuthed);
/*export const authedRateLimitedProcedure = ({ intervalInMs, limit }: IRateLimitOptions) =>
authedProcedure.use(isRateLimitedByUserIdMiddleware({ intervalInMs, limit }));*/
export const authedAdminProcedure = publicProcedure.use(captureErrorsMiddleware).use(isAdminMiddleware);
export const authedOrgAdminProcedure = publicProcedure.use(captureErrorsMiddleware).use(isOrgAdminMiddleware);
export default authedProcedure;

View File

@@ -0,0 +1,7 @@
import captureErrorsMiddleware from "../middlewares/captureErrorsMiddleware";
import perfMiddleware from "../middlewares/perfMiddleware";
import { tRPCContext } from "../trpc";
const publicProcedure = tRPCContext.procedure.use(captureErrorsMiddleware).use(perfMiddleware);
export default publicProcedure;

View File

@@ -0,0 +1,17 @@
/**
* This file contains the root router of your tRPC-backend
*/
import { router } from "../trpc";
import { viewerRouter } from "./viewer/_router";
/**
* Create your application's root router
* If you want to use SSG, you need export this
* @link https://trpc.io/docs/ssg
* @link https://trpc.io/docs/router
*/
export const appRouter = router({
viewer: viewerRouter,
});
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,500 @@
import authedProcedure from "../../procedures/authedProcedure";
import { importHandler, router } from "../../trpc";
import { ZAddSecondaryEmailInputSchema } from "./addSecondaryEmail.schema";
import { ZAppByIdInputSchema } from "./appById.schema";
import { ZAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
import { ZConnectAndJoinInputSchema } from "./connectAndJoin.schema";
import { ZConnectedCalendarsInputSchema } from "./connectedCalendars.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 { ZLocationOptionsInputSchema } from "./locationOptions.schema";
import { ZOutOfOfficeInputSchema, ZOutOfOfficeDelete } from "./outOfOffice.schema";
import { me } from "./procedures/me";
import { teamsAndUserProfilesQuery } from "./procedures/teamsAndUserProfilesQuery";
import { ZRoutingFormOrderInputSchema } from "./routingFormOrder.schema";
import { ZSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
import { ZSubmitFeedbackInputSchema } from "./submitFeedback.schema";
import { ZUpdateProfileInputSchema } from "./updateProfile.schema";
import { ZUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
import { ZWorkflowOrderInputSchema } from "./workflowOrder.schema";
const NAMESPACE = "loggedInViewer";
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
type AppsRouterHandlerCache = {
me?: typeof import("./me.handler").meHandler;
shouldVerifyEmail?: typeof import("./shouldVerifyEmail.handler").shouldVerifyEmailHandler;
deleteMe?: typeof import("./deleteMe.handler").deleteMeHandler;
deleteMeWithoutPassword?: typeof import("./deleteMeWithoutPassword.handler").deleteMeWithoutPasswordHandler;
connectedCalendars?: typeof import("./connectedCalendars.handler").connectedCalendarsHandler;
setDestinationCalendar?: typeof import("./setDestinationCalendar.handler").setDestinationCalendarHandler;
integrations?: typeof import("./integrations.handler").integrationsHandler;
appById?: typeof import("./appById.handler").appByIdHandler;
appCredentialsByType?: typeof import("./appCredentialsByType.handler").appCredentialsByTypeHandler;
stripeCustomer?: typeof import("./stripeCustomer.handler").stripeCustomerHandler;
updateProfile?: typeof import("./updateProfile.handler").updateProfileHandler;
eventTypeOrder?: typeof import("./eventTypeOrder.handler").eventTypeOrderHandler;
routingFormOrder?: typeof import("./routingFormOrder.handler").routingFormOrderHandler;
workflowOrder?: typeof import("./workflowOrder.handler").workflowOrderHandler;
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;
teamsAndUserProfilesQuery?: typeof import("./teamsAndUserProfilesQuery.handler").teamsAndUserProfilesQuery;
getUserTopBanners?: typeof import("./getUserTopBanners.handler").getUserTopBannersHandler;
connectAndJoin?: typeof import("./connectAndJoin.handler").Handler;
outOfOfficeCreate?: typeof import("./outOfOffice.handler").outOfOfficeCreate;
outOfOfficeEntriesList?: typeof import("./outOfOffice.handler").outOfOfficeEntriesList;
outOfOfficeEntryDelete?: typeof import("./outOfOffice.handler").outOfOfficeEntryDelete;
addSecondaryEmail?: typeof import("./addSecondaryEmail.handler").addSecondaryEmailHandler;
getTravelSchedules?: typeof import("./getTravelSchedules.handler").getTravelSchedulesHandler;
outOfOfficeReasonList?: typeof import("./outOfOfficeReasons.handler").outOfOfficeReasonList;
};
const UNSTABLE_HANDLER_CACHE: AppsRouterHandlerCache = {};
export const loggedInViewerRouter = router({
me,
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 });
}),
connectedCalendars: authedProcedure.input(ZConnectedCalendarsInputSchema).query(async ({ ctx, input }) => {
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, input });
}),
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 });
}),
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 });
}),
unlinkConnectedAccount: authedProcedure.mutation(async (opts) => {
const handler = await importHandler(
namespaced("unlinkConnectedAccount"),
() => import("./unlinkConnectedAccount.handler")
);
return handler(opts);
}),
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 });
}),
routingFormOrder: authedProcedure.input(ZRoutingFormOrderInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.routingFormOrder) {
UNSTABLE_HANDLER_CACHE.routingFormOrder = (
await import("./routingFormOrder.handler")
).routingFormOrderHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.routingFormOrder) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.routingFormOrder({ ctx, input });
}),
workflowOrder: authedProcedure.input(ZWorkflowOrderInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.workflowOrder) {
UNSTABLE_HANDLER_CACHE.workflowOrder = (await import("./workflowOrder.handler")).workflowOrderHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.workflowOrder) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.workflowOrder({ 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.input(ZLocationOptionsInputSchema).query(async ({ ctx, input }) => {
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, input });
}),
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 });
}),
getUserTopBanners: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.getUserTopBanners) {
UNSTABLE_HANDLER_CACHE.getUserTopBanners = (
await import("./getUserTopBanners.handler")
).getUserTopBannersHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getUserTopBanners) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getUserTopBanners({ ctx });
}),
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 });
}),
shouldVerifyEmail: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.shouldVerifyEmail) {
UNSTABLE_HANDLER_CACHE.shouldVerifyEmail = (
await import("./shouldVerifyEmail.handler")
).shouldVerifyEmailHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.shouldVerifyEmail) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.shouldVerifyEmail({ ctx });
}),
teamsAndUserProfilesQuery,
connectAndJoin: authedProcedure.input(ZConnectAndJoinInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.connectAndJoin) {
UNSTABLE_HANDLER_CACHE.connectAndJoin = (await import("./connectAndJoin.handler")).Handler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.connectAndJoin) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.connectAndJoin({ ctx, input });
}),
outOfOfficeCreate: authedProcedure.input(ZOutOfOfficeInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeCreate) {
UNSTABLE_HANDLER_CACHE.outOfOfficeCreate = (await import("./outOfOffice.handler")).outOfOfficeCreate;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeCreate) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.outOfOfficeCreate({ ctx, input });
}),
outOfOfficeEntriesList: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeEntriesList) {
UNSTABLE_HANDLER_CACHE.outOfOfficeEntriesList = (
await import("./outOfOffice.handler")
).outOfOfficeEntriesList;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeEntriesList) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.outOfOfficeEntriesList({ ctx });
}),
outOfOfficeEntryDelete: authedProcedure.input(ZOutOfOfficeDelete).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeEntryDelete) {
UNSTABLE_HANDLER_CACHE.outOfOfficeEntryDelete = (
await import("./outOfOffice.handler")
).outOfOfficeEntryDelete;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeEntryDelete) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.outOfOfficeEntryDelete({ ctx, input });
}),
addSecondaryEmail: authedProcedure.input(ZAddSecondaryEmailInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.addSecondaryEmail) {
UNSTABLE_HANDLER_CACHE.addSecondaryEmail = (
await import("./addSecondaryEmail.handler")
).addSecondaryEmailHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.addSecondaryEmail) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.addSecondaryEmail({ ctx, input });
}),
getTravelSchedules: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.getTravelSchedules) {
UNSTABLE_HANDLER_CACHE.getTravelSchedules = (
await import("./getTravelSchedules.handler")
).getTravelSchedulesHandler;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getTravelSchedules) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getTravelSchedules({ ctx });
}),
outOfOfficeReasonList: authedProcedure.query(async () => {
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeReasonList) {
UNSTABLE_HANDLER_CACHE.outOfOfficeReasonList = (
await import("./outOfOfficeReasons.handler")
).outOfOfficeReasonList;
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.outOfOfficeReasonList) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.outOfOfficeReasonList();
}),
});

View File

@@ -0,0 +1,57 @@
import type { GetServerSidePropsContext, NextApiResponse } from "next";
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TAddSecondaryEmailInputSchema } from "./addSecondaryEmail.schema";
type AddSecondaryEmailOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
res?: NextApiResponse | GetServerSidePropsContext["res"];
};
input: TAddSecondaryEmailInputSchema;
};
export const addSecondaryEmailHandler = async ({ ctx, input }: AddSecondaryEmailOptions) => {
const { user } = ctx;
const existingPrimaryEmail = await prisma.user.findUnique({
where: {
email: input.email,
},
});
if (existingPrimaryEmail) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Email already taken" });
}
const existingSecondaryEmail = await prisma.secondaryEmail.findUnique({
where: {
email: input.email,
},
});
if (existingSecondaryEmail) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Email already taken" });
}
const updatedData = await prisma.secondaryEmail.create({
data: { ...input, userId: user.id },
});
await sendEmailVerification({
email: updatedData.email,
username: user?.username ?? undefined,
language: user.locale,
secondaryEmailId: updatedData.id,
});
return {
data: updatedData,
message: "Secondary email added successfully",
};
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZAddSecondaryEmailInputSchema = z.object({
email: z.string(),
});
export type TAddSecondaryEmailInputSchema = z.infer<typeof ZAddSecondaryEmailInputSchema>;

View File

@@ -0,0 +1,32 @@
import getApps from "@calcom/app-store/utils";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
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 = await getUsersCredentials(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,
};
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZAppByIdInputSchema = z.object({
appId: z.string(),
});
export type TAppByIdInputSchema = z.infer<typeof ZAppByIdInputSchema>;

View File

@@ -0,0 +1,39 @@
import { UserRepository } from "@calcom/lib/server/repository/user";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
type AppCredentialsByTypeOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TAppCredentialsByTypeInputSchema;
};
/** Used for grabbing credentials on specific app pages */
export const appCredentialsByTypeHandler = async ({ ctx, input }: AppCredentialsByTypeOptions) => {
const { user } = ctx;
const userAdminTeams = await UserRepository.getUserAdminTeams(ctx.user.id);
const credentials = await prisma.credential.findMany({
where: {
OR: [
{ userId: user.id },
{
teamId: {
in: userAdminTeams,
},
},
],
type: input.appType,
},
});
// For app pages need to return which teams the user can install the app on
// return user.credentials.filter((app) => app.type == input.appType).map((credential) => credential.id);
return {
credentials,
userAdminTeams,
};
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZAppCredentialsByTypeInputSchema = z.object({
appType: z.string(),
});
export type TAppCredentialsByTypeInputSchema = z.infer<typeof ZAppCredentialsByTypeInputSchema>;

View File

@@ -0,0 +1,37 @@
import { prisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
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);
};

View File

@@ -0,0 +1,52 @@
import { getAppFromSlug } from "@calcom/app-store/utils";
import { type InvalidAppCredentialBannerProps } from "@calcom/features/users/components/InvalidAppCredentialsBanner";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/client";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type checkInvalidAppCredentialsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const checkInvalidAppCredentials = async ({ ctx }: checkInvalidAppCredentialsOptions) => {
const userId = ctx.user.id;
const apps = await prisma.credential.findMany({
where: {
OR: [
{
userId: userId,
},
{
team: {
members: {
some: {
userId: userId,
accepted: true,
role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
},
},
},
},
],
invalid: true,
},
select: {
appId: true,
},
});
const appNamesAndSlugs: InvalidAppCredentialBannerProps[] = [];
for (const app of apps) {
if (app.appId) {
const appId = app.appId;
const appMeta = await getAppFromSlug(appId);
const name = appMeta ? appMeta.name : appId;
appNamesAndSlugs.push({ slug: appId, name });
}
}
return appNamesAndSlugs;
};

View File

@@ -0,0 +1,217 @@
import { sendScheduledEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { prisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { TRPCError } from "@trpc/server";
import type { TConnectAndJoinInputSchema } from "./connectAndJoin.schema";
type Options = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TConnectAndJoinInputSchema;
};
export const Handler = async ({ ctx, input }: Options) => {
const { token } = input;
const { user } = ctx;
const isLoggedInUserPartOfOrg = !!user.organization.id;
if (!isLoggedInUserPartOfOrg) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Logged in user is not member of Organization" });
}
const tOrganizer = await getTranslation(user?.locale ?? "en", "common");
const instantMeetingToken = await prisma.instantMeetingToken.findUnique({
select: {
expires: true,
teamId: true,
booking: {
select: {
id: true,
status: true,
user: {
select: {
id: true,
},
},
},
},
},
where: {
token,
team: {
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
},
});
// Check if logged in user belong to current team
if (!instantMeetingToken) {
throw new TRPCError({ code: "BAD_REQUEST", message: "token_not_found" });
}
if (!instantMeetingToken.booking?.id) {
throw new TRPCError({ code: "FORBIDDEN", message: "token_invalid_expired" });
}
// Check if token has not expired
if (instantMeetingToken.expires < new Date()) {
throw new TRPCError({ code: "BAD_REQUEST", message: "token_invalid_expired" });
}
// Check if Booking is already accepted by any other user
let isBookingAlreadyAcceptedBySomeoneElse = false;
if (
instantMeetingToken.booking.status === BookingStatus.ACCEPTED &&
instantMeetingToken.booking?.user?.id !== user.id
) {
isBookingAlreadyAcceptedBySomeoneElse = true;
}
// Update User in Booking
const updatedBooking = await prisma.booking.update({
where: {
id: instantMeetingToken.booking.id,
},
data: {
...(isBookingAlreadyAcceptedBySomeoneElse
? { status: BookingStatus.ACCEPTED }
: {
status: BookingStatus.ACCEPTED,
user: {
connect: {
id: user.id,
},
},
}),
},
select: {
title: true,
description: true,
customInputs: true,
startTime: true,
references: true,
endTime: true,
attendees: true,
eventTypeId: true,
responses: true,
metadata: true,
eventType: {
select: {
id: true,
owner: true,
teamId: true,
title: true,
slug: true,
requiresConfirmation: true,
currency: true,
length: true,
description: true,
price: true,
bookingFields: true,
disableGuests: true,
metadata: true,
customInputs: true,
parentId: true,
},
},
location: true,
userId: true,
id: true,
uid: true,
status: true,
},
});
const locationVideoCallUrl = bookingMetadataSchema.parse(updatedBooking.metadata || {})?.videoCallUrl;
if (!locationVideoCallUrl) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "meeting_url_not_found" });
}
const videoCallReference = updatedBooking.references.find((reference) => reference.type.includes("_video"));
const videoCallData = {
type: videoCallReference?.type,
id: videoCallReference?.meetingId,
password: videoCallReference?.meetingPassword,
url: videoCallReference?.meetingUrl,
};
const { eventType } = updatedBooking;
// Send Scheduled Email to Organizer and Attendees
const translations = new Map();
const attendeesListPromises = updatedBooking.attendees.map(async (attendee) => {
const locale = attendee.locale ?? "en";
let translate = translations.get(locale);
if (!translate) {
translate = await getTranslation(locale, "common");
translations.set(locale, translate);
}
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate,
locale,
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const evt: CalendarEvent = {
type: updatedBooking?.eventType?.slug as string,
title: updatedBooking.title,
description: updatedBooking.description,
...getCalEventResponses({
bookingFields: eventType?.bookingFields ?? null,
booking: updatedBooking,
}),
customInputs: isPrismaObjOrUndefined(updatedBooking.customInputs),
startTime: updatedBooking.startTime.toISOString(),
endTime: updatedBooking.endTime.toISOString(),
organizer: {
email: user.email,
name: user.name || "Unnamed",
username: user.username || undefined,
timeZone: user.timeZone,
timeFormat: getTimeFormatStringFromUserTimeFormat(user.timeFormat),
language: { translate: tOrganizer, locale: user.locale ?? "en" },
},
attendees: attendeesList,
location: updatedBooking.location ?? "",
uid: updatedBooking.uid,
requiresConfirmation: false,
eventTypeId: eventType?.id,
videoCallData,
};
await sendScheduledEmails(
{
...evt,
},
undefined,
false,
false
);
return { isBookingAlreadyAcceptedBySomeoneElse, meetingUrl: locationVideoCallUrl };
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZConnectAndJoinInputSchema = z.object({
token: z.string(),
});
export type TConnectAndJoinInputSchema = z.infer<typeof ZConnectAndJoinInputSchema>;

View File

@@ -0,0 +1,28 @@
import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TConnectedCalendarsInputSchema } from "./connectedCalendars.schema";
type ConnectedCalendarsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TConnectedCalendarsInputSchema;
};
export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalendarsOptions) => {
const { user } = ctx;
const onboarding = input?.onboarding || false;
const { connectedCalendars, destinationCalendar } = await getConnectedDestinationCalendars(
user,
onboarding,
prisma
);
return {
connectedCalendars,
destinationCalendar,
};
};

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const ZConnectedCalendarsInputSchema = z
.object({
onboarding: z.boolean().optional(),
})
.optional();
export type TConnectedCalendarsInputSchema = z.infer<typeof ZConnectedCalendarsInputSchema>;

View File

@@ -0,0 +1,460 @@
import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { DailyLocationType } from "@calcom/core/location";
import { sendCancelledEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { deletePayment } from "@calcom/lib/payment/deletePayment";
import { getTranslation } from "@calcom/lib/server/i18n";
import { bookingMinimalSelect, prisma } from "@calcom/prisma";
import { AppCategories, BookingStatus } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils";
import { userMetadata } from "@calcom/prisma/zod-utils";
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;
};
type App = {
slug: string;
categories: AppCategories[];
dirName: string;
} | null;
const isVideoOrConferencingApp = (app: App) =>
app?.categories.includes(AppCategories.video) || app?.categories.includes(AppCategories.conferencing);
const getRemovedIntegrationNameFromAppSlug = (slug: string) =>
slug === "msteams" ? "office365_video" : slug.split("-")[0];
const locationsSchema = z.array(z.object({ type: z.string() }));
type TlocationsSchema = z.infer<typeof locationsSchema>;
export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOptions) => {
const { user } = ctx;
const { id, teamId } = input;
const credential = await prisma.credential.findFirst({
where: {
id: id,
...(teamId ? { teamId } : { userId: ctx.user.id }),
},
select: {
...credentialForCalendarServiceSelect,
app: {
select: {
slug: true,
categories: true,
dirName: true,
},
},
},
});
if (!credential) {
throw new TRPCError({ code: "NOT_FOUND" });
}
const eventTypes = await prisma.eventType.findMany({
where: {
OR: [
{
...(teamId ? { teamId } : { userId: ctx.user.id }),
},
// for managed events
{
parent: {
teamId,
},
},
],
},
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 it's a video, replace the location with Cal video
if (eventType.locations && isVideoOrConferencingApp(credential.app)) {
// Find the user's event types
const integrationQuery = getRemovedIntegrationNameFromAppSlug(credential.app?.slug ?? "");
// Check if the event type uses the deleted integration
// To avoid type errors, need to stringify and parse JSON to use array methods
const locations = locationsSchema.parse(eventType.locations);
const doesDailyVideoAlreadyExists = locations.some((location) =>
location.type.includes(DailyLocationType)
);
const updatedLocations: TlocationsSchema = locations.reduce((acc: TlocationsSchema, location) => {
if (location.type.includes(integrationQuery)) {
if (!doesDailyVideoAlreadyExists) acc.push({ type: DailyLocationType });
} else {
acc.push(location);
}
return acc;
}, []);
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) &&
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 (credential.app?.categories.includes(AppCategories.crm)) {
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const appSlugToDelete = credential.app?.slug;
if (appSlugToDelete) {
const appMetadata = removeAppFromEventTypeMetadata(appSlugToDelete, metadata);
await prisma.$transaction(async () => {
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
hidden: true,
metadata: {
...metadata,
apps: {
...appMetadata,
},
},
},
});
});
}
}
// 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)) {
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const appSlug = credential.app?.slug;
if (appSlug) {
const appMetadata = removeAppFromEventTypeMetadata(appSlug, metadata);
await prisma.$transaction(async () => {
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
hidden: true,
metadata: {
...metadata,
apps: {
...appMetadata,
},
},
},
});
// 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,
eventName: 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?.userPrimaryEmail ?? (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.destinationCalendar]
: booking.user?.destinationCalendar
? [booking.user?.destinationCalendar]
: [],
cancellationReason: "Payment method removed by organizer",
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
},
{
eventName: booking?.eventType?.eventName,
}
);
}
});
}
} else if (
appStoreMetadata[credential.app?.slug as keyof typeof appStoreMetadata]?.extendsFeature === "EventType"
) {
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const appSlug = credential.app?.slug;
if (appSlug) {
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
hidden: true,
metadata: {
...metadata,
apps: {
...metadata?.apps,
[appSlug]: undefined,
},
},
},
});
}
}
}
// 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",
},
});
deleteWebhookScheduledTriggers({
appId: credential.appId,
userId: teamId ? undefined : ctx.user.id,
teamId,
});
}
let metadata = userMetadata.parse(user.metadata);
if (credential.app?.slug === metadata?.defaultConferencingApp?.appSlug) {
metadata = {
...metadata,
defaultConferencingApp: undefined,
};
await prisma.user.update({
where: {
id: user.id,
},
data: {
metadata,
},
});
}
// Backwards compatibility. Selected calendars cascade on delete when deleting a credential
// If it's a calendar remove it from the SelectedCalendars
if (credential.app?.categories.includes(AppCategories.calendar)) {
try {
const calendar = await getCalendar(credential);
const calendars = await calendar?.listCalendars();
const calendarIds = calendars?.map((cal) => cal.externalId);
await prisma.selectedCalendar.deleteMany({
where: {
userId: user.id,
integration: credential.type as string,
externalId: {
in: calendarIds,
},
},
});
} catch (error) {
console.warn(
`Error deleting selected calendars for userId: ${user.id} integration: ${credential.type}`,
error
);
}
}
// Validated that credential is user's above
await prisma.credential.delete({
where: {
id: id,
},
});
};
const removeAppFromEventTypeMetadata = (
appSlugToDelete: string,
eventTypeMetadata: z.infer<typeof EventTypeMetaDataSchema>
) => {
const appMetadata = eventTypeMetadata?.apps
? Object.entries(eventTypeMetadata.apps).reduce((filteredApps, [appName, appData]) => {
if (appName !== appSlugToDelete) {
filteredApps[appName as keyof typeof eventTypeMetadata.apps] = appData;
}
return filteredApps;
}, {} as z.infer<typeof EventTypeAppMetadataSchema>)
: {};
return appMetadata;
};

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const ZDeleteCredentialInputSchema = z.object({
id: z.number(),
externalId: z.string().optional(),
teamId: z.number().optional(),
});
export type TDeleteCredentialInputSchema = z.infer<typeof ZDeleteCredentialInputSchema>;

View File

@@ -0,0 +1,87 @@
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { deleteUser } from "@calcom/features/users/lib/userDeletionService";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { HttpError } from "@calcom/lib/http-error";
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import { prisma } from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
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) => {
// TODO: First check password is part of input and meets requirements.
// Check if input.password is correct
const user = await prisma.user.findUnique({
where: {
email: ctx.user.email.toLowerCase(),
},
select: {
identityProvider: true,
twoFactorEnabled: true,
twoFactorSecret: true,
password: true,
email: true,
metadata: true,
id: true,
},
});
if (!user) {
throw new HttpError({ statusCode: 404, message: ErrorCode.UserNotFound });
}
if (user.identityProvider !== IdentityProvider.CAL) {
throw new HttpError({ statusCode: 400, message: ErrorCode.ThirdPartyIdentityProviderEnabled });
}
if (!user.password?.hash) {
throw new HttpError({ statusCode: 400, message: ErrorCode.UserMissingPassword });
}
const isCorrectPassword = await verifyPassword(input.password, user.password.hash);
if (!isCorrectPassword) {
throw new HttpError({ statusCode: 403, message: ErrorCode.IncorrectPassword });
}
if (user.twoFactorEnabled) {
if (!input.totpCode) {
throw new HttpError({ statusCode: 400, message: 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 = totpAuthenticatorCheck(input.totpCode, secret);
if (!isValidToken) {
throw new HttpError({ statusCode: 403, message: ErrorCode.IncorrectTwoFactorCode });
}
}
await deleteUser(user);
return;
};

View File

@@ -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>;

View File

@@ -0,0 +1,45 @@
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 { IdentityProvider } from "@calcom/prisma/enums";
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;
};

View File

@@ -0,0 +1,67 @@
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(
input.ids.reverse().map((id, position) => {
return prisma.eventType.update({
where: {
id,
},
data: {
position,
},
});
})
);
};

View File

@@ -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>;

View File

@@ -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",
});
}
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZGetCalVideoRecordingsInputSchema = z.object({
roomName: z.string(),
});
export type TGetCalVideoRecordingsInputSchema = z.infer<typeof ZGetCalVideoRecordingsInputSchema>;

View File

@@ -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 { WithSession } from "../../createContext";
import type { TGetDownloadLinkOfCalVideoRecordingsInputSchema } from "./getDownloadLinkOfCalVideoRecordings.schema";
type GetDownloadLinkOfCalVideoRecordingsHandlerOptions = {
ctx: WithSession;
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",
});
}
};

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const ZGetDownloadLinkOfCalVideoRecordingsInputSchema = z.object({
recordingId: z.string(),
});
export type TGetDownloadLinkOfCalVideoRecordingsInputSchema = z.infer<
typeof ZGetDownloadLinkOfCalVideoRecordingsInputSchema
>;

View File

@@ -0,0 +1,24 @@
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type GetTravelSchedulesOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const getTravelSchedulesHandler = async ({ ctx }: GetTravelSchedulesOptions) => {
const allTravelSchedules = await prisma.travelSchedule.findMany({
where: {
userId: ctx.user.id,
},
select: {
id: true,
startDate: true,
endDate: true,
timeZone: true,
},
});
return allTravelSchedules;
};

View File

@@ -0,0 +1,67 @@
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { prisma } from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { checkIfOrgNeedsUpgradeHandler } from "../viewer/organizations/checkIfOrgNeedsUpgrade.handler";
import { getUpgradeableHandler } from "../viewer/teams/getUpgradeable.handler";
import { checkInvalidAppCredentials } from "./checkForInvalidAppCredentials";
import { shouldVerifyEmailHandler } from "./shouldVerifyEmail.handler";
type Props = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
const checkInvalidGoogleCalendarCredentials = async ({ ctx }: Props) => {
const userCredentials = await prisma.credential.findMany({
where: {
userId: ctx.user.id,
type: "google_calendar",
},
select: credentialForCalendarServiceSelect,
});
const calendarCredentials = getCalendarCredentials(userCredentials);
const { connectedCalendars } = await getConnectedCalendars(
calendarCredentials,
ctx.user.selectedCalendars,
ctx.user.destinationCalendar?.externalId
);
return connectedCalendars.some((calendar) => !!calendar.error);
};
export const getUserTopBannersHandler = async ({ ctx }: Props) => {
const upgradeableTeamMememberships = getUpgradeableHandler({ userId: ctx.user.id });
const upgradeableOrgMememberships = checkIfOrgNeedsUpgradeHandler({ ctx });
const shouldEmailVerify = shouldVerifyEmailHandler({ ctx });
const isInvalidCalendarCredential = checkInvalidGoogleCalendarCredentials({ ctx });
const appsWithInavlidCredentials = checkInvalidAppCredentials({ ctx });
const [
teamUpgradeBanner,
orgUpgradeBanner,
verifyEmailBanner,
calendarCredentialBanner,
invalidAppCredentialBanners,
] = await Promise.allSettled([
upgradeableTeamMememberships,
upgradeableOrgMememberships,
shouldEmailVerify,
isInvalidCalendarCredential,
appsWithInavlidCredentials,
]);
return {
teamUpgradeBanner: teamUpgradeBanner.status === "fulfilled" ? teamUpgradeBanner.value : [],
orgUpgradeBanner: orgUpgradeBanner.status === "fulfilled" ? orgUpgradeBanner.value : [],
verifyEmailBanner: verifyEmailBanner.status === "fulfilled" ? !verifyEmailBanner.value.isVerified : false,
calendarCredentialBanner:
calendarCredentialBanner.status === "fulfilled" ? calendarCredentialBanner.value : false,
invalidAppCredentialBanners:
invalidAppCredentialBanners.status === "fulfilled" ? invalidAppCredentialBanners.value : [],
};
};

View File

@@ -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;
};

View File

@@ -0,0 +1,252 @@
import type { Prisma } from "@prisma/client";
import appStore from "@calcom/app-store";
import type { TDependencyData } from "@calcom/app-store/_appRegistry";
import type { CredentialOwner } from "@calcom/app-store/types";
import { getAppFromSlug } from "@calcom/app-store/utils";
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { PaymentApp } from "@calcom/types/PaymentService";
import type { TIntegrationsInputSchema } from "./integrations.schema";
type IntegrationsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TIntegrationsInputSchema;
};
type TeamQuery = Prisma.TeamGetPayload<{
select: {
id: true;
credentials: {
select: typeof import("@calcom/prisma/selects/credential").credentialForCalendarServiceSelect;
};
name: true;
logoUrl: true;
members: {
select: {
role: true;
};
};
};
}>;
// type TeamQueryWithParent = TeamQuery & {
// parent?: TeamQuery | null;
// };
export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) => {
const { user } = ctx;
const {
variant,
exclude,
onlyInstalled,
includeTeamInstalledApps,
extendsFeature,
teamId,
sortByMostPopular,
appId,
} = input;
let credentials = await getUsersCredentials(user);
let userTeams: TeamQuery[] = [];
if (includeTeamInstalledApps || teamId) {
const teamsQuery = await prisma.team.findMany({
where: {
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
select: {
id: true,
credentials: {
select: credentialForCalendarServiceSelect,
},
name: true,
logoUrl: true,
members: {
where: {
userId: user.id,
},
select: {
role: true,
},
},
parent: {
select: {
id: true,
credentials: {
select: credentialForCalendarServiceSelect,
},
name: true,
logoUrl: true,
members: {
where: {
userId: user.id,
},
select: {
role: true,
},
},
},
},
},
});
// If a team is a part of an org then include those apps
// Don't want to iterate over these parent teams
const filteredTeams: TeamQuery[] = [];
const parentTeams: TeamQuery[] = [];
// Only loop and grab parent teams if a teamId was given. If not then all teams will be queried
if (teamId) {
teamsQuery.forEach((team) => {
if (team?.parent) {
const { parent, ...filteredTeam } = team;
filteredTeams.push(filteredTeam);
// Only add parent team if it's not already in teamsQuery
if (!teamsQuery.some((t) => t.id === parent.id)) {
parentTeams.push(parent);
}
}
});
}
userTeams = [...teamsQuery, ...parentTeams];
const teamAppCredentials: CredentialPayload[] = userTeams.flatMap((teamApp) => {
return teamApp.credentials ? teamApp.credentials.flat() : [];
});
if (!includeTeamInstalledApps || teamId) {
credentials = teamAppCredentials;
} else {
credentials = credentials.concat(teamAppCredentials);
}
}
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
filterOnCredentials: onlyInstalled,
...(appId ? { where: { slug: appId } } : {}),
});
//TODO: Refactor this to pick up only needed fields and prevent more leaking
let apps = await Promise.all(
enabledApps.map(async ({ credentials: _, credential, key: _2 /* don't leak to frontend */, ...app }) => {
const userCredentialIds = credentials.filter((c) => c.appId === app.slug && !c.teamId).map((c) => c.id);
const invalidCredentialIds = credentials
.filter((c) => c.appId === app.slug && c.invalid)
.map((c) => c.id);
const teams = await Promise.all(
credentials
.filter((c) => c.appId === app.slug && c.teamId)
.map(async (c) => {
const team = userTeams.find((team) => team.id === c.teamId);
if (!team) {
return null;
}
return {
teamId: team.id,
name: team.name,
logoUrl: team.logoUrl,
credentialId: c.id,
isAdmin:
team.members[0].role === MembershipRole.ADMIN ||
team.members[0].role === MembershipRole.OWNER,
};
})
);
// type infer as CredentialOwner
const credentialOwner: CredentialOwner = {
name: user.name,
avatar: user.avatar,
};
// We need to know if app is payment type
// undefined it means that app don't require app/setup/page
let isSetupAlready = undefined;
if (credential && app.categories.includes("payment")) {
const paymentApp = (await appStore[app.dirName as keyof typeof appStore]?.()) as PaymentApp | null;
if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
const PaymentService = paymentApp.lib.PaymentService;
const paymentInstance = new PaymentService(credential);
isSetupAlready = paymentInstance.isSetupAlready();
}
}
let dependencyData: TDependencyData = [];
if (app.dependencies?.length) {
dependencyData = app.dependencies.map((dependency) => {
const dependencyInstalled = enabledApps.some(
(dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency
);
// If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app.
const dependencyName = getAppFromSlug(dependency)?.name;
return { name: dependencyName, installed: dependencyInstalled };
});
}
return {
...app,
...(teams.length && {
credentialOwner,
}),
userCredentialIds,
invalidCredentialIds,
teams,
isInstalled: !!userCredentialIds.length || !!teams.length || app.isGlobal,
isSetupAlready,
...(app.dependencies && { dependencyData }),
};
})
);
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.userCredentialIds.length > 0 || item.teams.length || item.isGlobal ? [item] : []
);
}
if (extendsFeature) {
apps = apps
.filter((app) => app.extendsFeature?.includes(extendsFeature))
.map((app) => ({
...app,
isInstalled: !!app.userCredentialIds?.length || !!app.teams?.length || app.isGlobal,
}));
}
if (sortByMostPopular) {
const installCountPerApp = await getInstallCountPerApp();
// sort the apps array by the most popular apps
apps.sort((a, b) => {
const aCount = installCountPerApp[a.slug] || 0;
const bCount = installCountPerApp[b.slug] || 0;
return bCount - aCount;
});
}
return {
items: apps,
};
};

View File

@@ -0,0 +1,17 @@
import { z } from "zod";
import { AppCategories } from "@calcom/prisma/enums";
export const ZIntegrationsInputSchema = z.object({
variant: z.string().optional(),
exclude: z.array(z.string()).optional(),
onlyInstalled: z.boolean().optional(),
includeTeamInstalledApps: z.boolean().optional(),
extendsFeature: z.literal("EventType").optional(),
teamId: z.union([z.number(), z.null()]).optional(),
sortByMostPopular: z.boolean().optional(),
categories: z.nativeEnum(AppCategories).array().optional(),
appId: z.string().optional(),
});
export type TIntegrationsInputSchema = z.infer<typeof ZIntegrationsInputSchema>;

View File

@@ -0,0 +1,30 @@
import { getLocationGroupedOptions } from "@calcom/app-store/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TLocationOptionsInputSchema } from "./locationOptions.schema";
type LocationOptionsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TLocationOptionsInputSchema;
};
export const locationOptionsHandler = async ({ ctx, input }: LocationOptionsOptions) => {
const { teamId } = input;
const t = await getTranslation(ctx.user.locale ?? "en", "common");
const locationOptions = await getLocationGroupedOptions(teamId ? { teamId } : { userId: ctx.user.id }, t);
// If it is a team event then move the "use host location" option to top
if (input.teamId) {
const conferencingIndex = locationOptions.findIndex((option) => option.label === "Conferencing");
if (conferencingIndex !== -1) {
const conferencingObject = locationOptions.splice(conferencingIndex, 1)[0];
locationOptions.unshift(conferencingObject);
}
}
return locationOptions;
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZLocationOptionsInputSchema = z.object({
teamId: z.number().optional(),
});
export type TLocationOptionsInputSchema = z.infer<typeof ZLocationOptionsInputSchema>;

View File

@@ -0,0 +1,157 @@
import type { Session } from "next-auth";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { userMetadata } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TMeInputSchema } from "./me.schema";
type MeOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
session: Session;
};
input: TMeInputSchema;
};
export const meHandler = async ({ ctx, input }: MeOptions) => {
const crypto = await import("crypto");
const { user: sessionUser, session } = ctx;
const allUserEnrichedProfiles = await ProfileRepository.findAllProfilesForUserIncludingMovedUser(
sessionUser
);
const user = await UserRepository.enrichUserWithTheProfile({
user: sessionUser,
upId: session.upId,
});
const secondaryEmails = await prisma.secondaryEmail.findMany({
where: {
userId: user.id,
},
select: {
id: true,
email: true,
emailVerified: true,
},
});
let passwordAdded = false;
if (user.identityProvider !== IdentityProvider.CAL && input?.includePasswordAdded) {
const userWithPassword = await prisma.user.findUnique({
where: {
id: user.id,
},
select: {
password: true,
},
});
if (userWithPassword?.password?.hash) {
passwordAdded = true;
}
}
let identityProviderEmail = "";
if (user.identityProviderId) {
const account = await prisma.account.findUnique({
where: {
provider_providerAccountId: {
provider: user.identityProvider.toLocaleLowerCase(),
providerAccountId: user.identityProviderId,
},
},
select: { providerEmail: true },
});
identityProviderEmail = account?.providerEmail || "";
}
const additionalUserInfo = await prisma.user.findFirst({
where: {
id: user.id,
},
select: {
bookings: {
select: { id: true },
},
selectedCalendars: true,
teams: {
select: {
team: {
select: {
id: true,
eventTypes: true,
},
},
},
},
eventTypes: {
select: { id: true },
},
},
});
let sumOfTeamEventTypes = 0;
for (const team of additionalUserInfo?.teams || []) {
for (const _eventType of team.team.eventTypes) {
sumOfTeamEventTypes++;
}
}
const userMetadataPrased = userMetadata.parse(user.metadata);
// 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,
email: user.email,
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
emailVerified: user.emailVerified,
startTime: user.startTime,
endTime: user.endTime,
bufferTime: user.bufferTime,
locale: user.locale,
timeFormat: user.timeFormat,
timeZone: user.timeZone,
avatar: getUserAvatarUrl(user),
avatarUrl: user.avatarUrl,
createdDate: user.createdDate,
trialEndsAt: user.trialEndsAt,
defaultScheduleId: user.defaultScheduleId,
completedOnboarding: user.completedOnboarding,
twoFactorEnabled: user.twoFactorEnabled,
disableImpersonation: user.disableImpersonation,
identityProvider: user.identityProvider,
identityProviderEmail,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
bio: user.bio,
weekStart: user.weekStart,
theme: user.theme,
appTheme: user.appTheme,
hideBranding: user.hideBranding,
metadata: user.metadata,
defaultBookerLayouts: user.defaultBookerLayouts,
allowDynamicBooking: user.allowDynamicBooking,
allowSEOIndexing: user.allowSEOIndexing,
receiveMonthlyDigestEmail: user.receiveMonthlyDigestEmail,
organizationId: user.profile?.organizationId ?? null,
organization: user.organization,
username: user.profile?.username ?? user.username ?? null,
profile: user.profile ?? null,
profiles: allUserEnrichedProfiles,
secondaryEmails,
sumOfBookings: additionalUserInfo?.bookings.length,
sumOfCalendars: additionalUserInfo?.selectedCalendars.length,
sumOfTeams: additionalUserInfo?.teams.length,
sumOfEventTypes: additionalUserInfo?.eventTypes.length,
isPremium: userMetadataPrased?.isPremium,
sumOfTeamEventTypes,
...(passwordAdded ? { passwordAdded } : {}),
};
};

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const ZMeInputSchema = z
.object({
includePasswordAdded: z.boolean().optional(),
})
.optional();
export type TMeInputSchema = z.infer<typeof ZMeInputSchema>;

View File

@@ -0,0 +1,260 @@
import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs";
import { sendBookingRedirectNotification } from "@calcom/emails";
import { getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TOutOfOfficeDelete, TOutOfOfficeInputSchema } from "./outOfOffice.schema";
type TBookingRedirect = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TOutOfOfficeInputSchema;
};
export const outOfOfficeCreate = async ({ ctx, input }: TBookingRedirect) => {
const { startDate, endDate } = input.dateRange;
if (!startDate || !endDate) {
throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_and_end_date_required" });
}
const inputStartTime = dayjs(startDate).startOf("day");
const inputEndTime = dayjs(endDate).endOf("day");
const offset = dayjs(inputStartTime).utcOffset();
// If start date is after end date throw error
if (inputStartTime.isAfter(inputEndTime)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_must_be_before_end_date" });
}
// If start date is before to today throw error
// Since this validation is done using server tz, we need to account for the offset
if (
inputStartTime.isBefore(
dayjs()
.startOf("day")
.subtract(Math.abs(offset) * 60, "minute")
)
) {
throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_must_be_in_the_future" });
}
let toUserId;
if (input.toTeamUserId) {
const user = await prisma.user.findUnique({
where: {
id: input.toTeamUserId,
/** You can only create OOO for members of teams you belong to */
teams: {
some: {
team: {
members: {
some: {
userId: ctx.user.id,
accepted: true,
},
},
},
},
},
},
select: {
id: true,
},
});
if (!user) {
throw new TRPCError({ code: "NOT_FOUND", message: "user_not_found" });
}
toUserId = user?.id;
}
// Validate if OOO entry for these dates already exists
const outOfOfficeEntry = await prisma.outOfOfficeEntry.findFirst({
where: {
AND: [
{ userId: ctx.user.id },
{
OR: [
{
start: {
lt: inputEndTime.toISOString(), //existing start is less than or equal to input end time
},
end: {
gt: inputStartTime.toISOString(), //existing end is greater than or equal to input start time
},
},
{
//existing start is within the new input range
start: {
gt: inputStartTime.toISOString(),
lt: inputEndTime.toISOString(),
},
},
{
//existing end is within the new input range
end: {
gt: inputStartTime.toISOString(),
lt: inputEndTime.toISOString(),
},
},
],
},
],
},
});
// don't allow overlapping entries
if (outOfOfficeEntry) {
throw new TRPCError({ code: "CONFLICT", message: "out_of_office_entry_already_exists" });
}
if (!input.reasonId) {
throw new TRPCError({ code: "BAD_REQUEST", message: "reason_id_required" });
}
// Prevent infinite redirects but consider time ranges
const existingOutOfOfficeEntry = await prisma.outOfOfficeEntry.findFirst({
select: {
userId: true,
toUserId: true,
},
where: {
userId: toUserId,
toUserId: ctx.user.id,
// Check for time overlap or collision
OR: [
// Outside of range
{
AND: [
{ start: { lte: inputEndTime.toISOString() } },
{ end: { gte: inputStartTime.toISOString() } },
],
},
// Inside of range
{
AND: [
{ start: { gte: inputStartTime.toISOString() } },
{ end: { lte: inputEndTime.toISOString() } },
],
},
],
},
});
// don't allow infinite redirects
if (existingOutOfOfficeEntry) {
throw new TRPCError({ code: "BAD_REQUEST", message: "booking_redirect_infinite_not_allowed" });
}
const startDateUtc = dayjs.utc(startDate).add(input.offset, "minute");
const endDateUtc = dayjs.utc(endDate).add(input.offset, "minute");
const createdRedirect = await prisma.outOfOfficeEntry.create({
data: {
uuid: uuidv4(),
start: startDateUtc.startOf("day").toISOString(),
end: endDateUtc.endOf("day").toISOString(),
notes: input.notes,
userId: ctx.user.id,
reasonId: input.reasonId,
toUserId: toUserId,
createdAt: new Date(),
updatedAt: new Date(),
},
});
if (toUserId) {
// await send email to notify user
const userToNotify = await prisma.user.findFirst({
where: {
id: toUserId,
},
select: {
email: true,
},
});
const t = await getTranslation(ctx.user.locale ?? "en", "common");
const formattedStartDate = new Intl.DateTimeFormat("en-US").format(createdRedirect.start);
const formattedEndDate = new Intl.DateTimeFormat("en-US").format(createdRedirect.end);
if (userToNotify?.email) {
await sendBookingRedirectNotification({
language: t,
fromEmail: ctx.user.email,
toEmail: userToNotify.email,
toName: ctx.user.username || "",
dates: `${formattedStartDate} - ${formattedEndDate}`,
});
}
}
return {};
};
type TBookingRedirectDelete = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TOutOfOfficeDelete;
};
export const outOfOfficeEntryDelete = async ({ ctx, input }: TBookingRedirectDelete) => {
if (!input.outOfOfficeUid) {
throw new TRPCError({ code: "BAD_REQUEST", message: "out_of_office_id_required" });
}
const deletedOutOfOfficeEntry = await prisma.outOfOfficeEntry.delete({
where: {
uuid: input.outOfOfficeUid,
/** Validate outOfOfficeEntry belongs to the user deleting it */
userId: ctx.user.id,
},
});
if (!deletedOutOfOfficeEntry) {
throw new TRPCError({ code: "NOT_FOUND", message: "booking_redirect_not_found" });
}
return {};
};
export const outOfOfficeEntriesList = async ({ ctx }: { ctx: { user: NonNullable<TrpcSessionUser> } }) => {
const outOfOfficeEntries = await prisma.outOfOfficeEntry.findMany({
where: {
userId: ctx.user.id,
end: {
gte: new Date().toISOString(),
},
},
orderBy: {
start: "asc",
},
select: {
id: true,
uuid: true,
start: true,
end: true,
toUserId: true,
toUser: {
select: {
username: true,
},
},
reason: {
select: {
id: true,
emoji: true,
reason: true,
userId: true,
},
},
notes: true,
},
});
return outOfOfficeEntries;
};

View File

@@ -0,0 +1,20 @@
import { z } from "zod";
export const ZOutOfOfficeInputSchema = z.object({
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
}),
offset: z.number(),
toTeamUserId: z.number().nullable(),
reasonId: z.number(),
notes: z.string().nullable().optional(),
});
export type TOutOfOfficeInputSchema = z.infer<typeof ZOutOfOfficeInputSchema>;
export const ZOutOfOfficeDelete = z.object({
outOfOfficeUid: z.string(),
});
export type TOutOfOfficeDelete = z.infer<typeof ZOutOfOfficeDelete>;

View File

@@ -0,0 +1,11 @@
import prisma from "@calcom/prisma";
export const outOfOfficeReasonList = async () => {
const outOfOfficeReasons = await prisma.outOfOfficeReason.findMany({
where: {
enabled: true,
},
});
return outOfOfficeReasons;
};

View File

@@ -0,0 +1,8 @@
import authedProcedure from "../../../procedures/authedProcedure";
import { ZMeInputSchema } from "../me.schema";
export const me = authedProcedure.input(ZMeInputSchema).query(async ({ ctx, input }) => {
const handler = (await import("../me.handler")).meHandler;
return handler({ ctx, input });
});

View File

@@ -0,0 +1,10 @@
import authedProcedure from "../../../procedures/authedProcedure";
import { ZTeamsAndUserProfilesQueryInputSchema } from "../teamsAndUserProfilesQuery.schema";
export const teamsAndUserProfilesQuery = authedProcedure
.input(ZTeamsAndUserProfilesQueryInputSchema)
.query(async ({ ctx, input }) => {
const handler = (await import("../teamsAndUserProfilesQuery.handler")).teamsAndUserProfilesQuery;
return handler({ ctx, input });
});

View File

@@ -0,0 +1,72 @@
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TRoutingFormOrderInputSchema } from "./routingFormOrder.schema";
type RoutingFormOrderOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TRoutingFormOrderInputSchema;
};
export const routingFormOrderHandler = async ({ ctx, input }: RoutingFormOrderOptions) => {
const { user } = ctx;
const forms = await prisma.app_RoutingForms_Form.findMany({
where: {
OR: [
{
userId: user.id,
},
{
team: {
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
},
],
},
orderBy: {
createdAt: "desc",
},
include: {
team: {
include: {
members: true,
},
},
_count: {
select: {
responses: true,
},
},
},
});
const allFormIds = new Set(forms.map((form) => form.id));
if (input.ids.some((id) => !allFormIds.has(id))) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
await Promise.all(
input.ids.reverse().map((id, position) => {
return prisma.app_RoutingForms_Form.update({
where: {
id: id,
},
data: {
position,
},
});
})
);
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZRoutingFormOrderInputSchema = z.object({
ids: z.array(z.string()),
});
export type TRoutingFormOrderInputSchema = z.infer<typeof ZRoutingFormOrderInputSchema>;

View File

@@ -0,0 +1,77 @@
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
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 SessionUser = NonNullable<TrpcSessionUser>;
type User = {
id: SessionUser["id"];
selectedCalendars: SessionUser["selectedCalendars"];
};
type SetDestinationCalendarOptions = {
ctx: {
user: User;
};
input: TSetDestinationCalendarInputSchema;
};
export const setDestinationCalendarHandler = async ({ ctx, input }: SetDestinationCalendarOptions) => {
const { user } = ctx;
const { integration, externalId, eventTypeId } = input;
const credentials = await getUsersCredentials(user);
const calendarCredentials = getCalendarCredentials(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}` });
}
const primaryEmail = allCals.find((cal) => cal.primary && cal.credentialId === credentialId)?.email ?? null;
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,
primaryEmail,
},
create: {
...where,
integration,
externalId,
credentialId,
primaryEmail,
},
});
};

View File

@@ -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>;

View File

@@ -0,0 +1,21 @@
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type ShouldVerifyEmailType = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const shouldVerifyEmailHandler = async ({ ctx }: ShouldVerifyEmailType) => {
const { user } = ctx;
const isVerified = !!user.emailVerified;
const isCalProvider = user.identityProvider === "CAL"; // We dont need to verify on OAUTH providers as they are already verified by the provider
const obj = {
id: user.id,
email: user.email,
isVerified: isVerified || !isCalProvider,
};
return obj;
};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,67 @@
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);
let stripeCustomerId = metadata?.stripeCustomerId;
if (!stripeCustomerId) {
// Create stripe customer
const customer = await stripe.customers.create({
metadata: {
userId: userId.toString(),
},
});
await prisma.user.update({
where: {
id: userId,
},
data: {
metadata: {
...metadata,
stripeCustomerId: customer.id,
},
},
});
stripeCustomerId = customer.id;
}
// Fetch stripe customer
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,
};
};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,38 @@
import dayjs from "@calcom/dayjs";
import { sendFeedbackEmail } from "@calcom/emails";
import { sendFeedbackFormbricks } from "@calcom/lib/formbricks";
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.NEXT_PUBLIC_FORMBRICKS_HOST_URL && process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID)
sendFeedbackFormbricks(ctx.user.id, feedback);
if (process.env.SEND_FEEDBACK_EMAIL && comment) sendFeedbackEmail(feedback);
};

View File

@@ -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>;

View File

@@ -0,0 +1,103 @@
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { PrismaClient } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TTeamsAndUserProfilesQueryInputSchema } from "./teamsAndUserProfilesQuery.schema";
type TeamsAndUserProfileOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
prisma: PrismaClient;
};
input: TTeamsAndUserProfilesQueryInputSchema;
};
export const teamsAndUserProfilesQuery = async ({ ctx, input }: TeamsAndUserProfileOptions) => {
const { prisma } = ctx;
const user = await prisma.user.findUnique({
where: {
id: ctx.user.id,
},
select: {
avatarUrl: true,
id: true,
username: true,
name: true,
teams: {
where: {
accepted: true,
},
select: {
role: true,
team: {
select: {
id: true,
isOrganization: true,
logoUrl: true,
name: true,
slug: true,
metadata: true,
parentId: true,
members: {
select: {
userId: true,
},
},
},
},
},
},
},
});
if (!user) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
let teamsData;
if (input?.includeOrg) {
teamsData = user.teams.map((membership) => ({
...membership,
team: {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
},
}));
} else {
teamsData = user.teams
.filter((membership) => !membership.team.isOrganization)
.map((membership) => ({
...membership,
team: {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
},
}));
}
return [
{
teamId: null,
name: user.name,
slug: user.username,
image: getUserAvatarUrl({
avatarUrl: user.avatarUrl,
}),
readOnly: false,
},
...teamsData.map((membership) => ({
teamId: membership.team.id,
name: membership.team.name,
slug: membership.team.slug ? `team/${membership.team.slug}` : null,
image: getPlaceholderAvatar(membership.team.logoUrl, membership.team.name),
role: membership.role,
readOnly: !withRoleCanCreateEntity(membership.role),
})),
];
};

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const ZTeamsAndUserProfilesQueryInputSchema = z
.object({
includeOrg: z.boolean().optional(),
})
.optional();
export type TTeamsAndUserProfilesQueryInputSchema = z.infer<typeof ZTeamsAndUserProfilesQueryInputSchema>;

View File

@@ -0,0 +1,71 @@
import prismock from "../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { IdentityProvider } from "@calcom/prisma/enums";
import unlinkConnectedAccountHandler from "./unlinkConnectedAccount.handler";
const buildOrgMockData = () => ({ id: null, isOrgAdmin: false, metadata: {}, requestedSlug: null });
const buildProfileMockData = () => ({
username: "test",
upId: "usr-xx",
id: null,
organizationId: null,
organization: null,
name: "Test User",
avatarUrl: null,
startTime: 0,
endTime: 1440,
bufferTime: 0,
});
async function buildMockData(
identityProvider: IdentityProvider = IdentityProvider.GOOGLE,
identityProviderId: string | null = null
) {
const promise = await prismock.user.create({
data: {
id: 1,
username: "test",
name: "Test User",
email: "test@example.com",
identityProvider,
identityProviderId,
},
});
const user = await promise;
return {
...user,
organization: buildOrgMockData(),
defaultBookerLayouts: null,
selectedCalendars: [],
destinationCalendar: null,
profile: buildProfileMockData(),
avatar: "",
locale: "en",
};
}
describe("unlinkConnectedAccount.handler", () => {
it("Should response with a success message when unlinking an Google account", async () => {
const user = await buildMockData(IdentityProvider.GOOGLE, "123456789012345678901");
const response = await unlinkConnectedAccountHandler({ ctx: { user } });
expect(response).toMatchInlineSnapshot(`
{
"message": "account_unlinked_success",
}
`);
});
it("Should respond with an error message if unlink was unsucessful", async () => {
const user = await buildMockData(IdentityProvider.CAL);
const response = await unlinkConnectedAccountHandler({ ctx: { user } });
expect(response).toMatchInlineSnapshot(`
{
"message": "account_unlinked_error",
}
`);
});
});

View File

@@ -0,0 +1,44 @@
import type { GetServerSidePropsContext, NextApiResponse } from "next";
import { prisma } from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type UpdateProfileOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
res?: NextApiResponse | GetServerSidePropsContext["res"];
};
};
const unlinkConnectedAccount = async ({ ctx }: UpdateProfileOptions) => {
const { user } = ctx;
// Unlink the account
const CalComAdapter = (await import("@calcom/features/auth/lib/next-auth-custom-adapter")).default;
const calcomAdapter = CalComAdapter(prisma);
// If it fails to delete, don't stop because the users login data might not be present
try {
await calcomAdapter.unlinkAccount({
provider: user.identityProvider.toLocaleLowerCase(),
providerAccountId: user.identityProviderId || "",
});
} catch {
// Fail silenty if we don't have an record in the account table
}
// Fall back to the default identity provider
const _user = await prisma.user.update({
where: {
id: user.id,
identityProvider: IdentityProvider.GOOGLE,
identityProviderId: { not: null },
},
data: {
identityProvider: IdentityProvider.CAL,
identityProviderId: null,
},
});
if (!_user) return { message: "account_unlinked_error" };
return { message: "account_unlinked_success" };
};
export default unlinkConnectedAccount;

View File

@@ -0,0 +1,401 @@
import { Prisma } from "@prisma/client";
// eslint-disable-next-line no-restricted-imports
import { keyBy } from "lodash";
import type { GetServerSidePropsContext, NextApiResponse } from "next";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import { sendChangeOfEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
import slugify from "@calcom/lib/slugify";
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
import { prisma } from "@calcom/prisma";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import { getDefaultScheduleId } from "../viewer/availability/util";
import { updateUserMetadataAllowedKeys, type TUpdateProfileInputSchema } from "./updateProfile.schema";
const log = logger.getSubLogger({ prefix: ["updateProfile"] });
type UpdateProfileOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
res?: NextApiResponse | GetServerSidePropsContext["res"];
};
input: TUpdateProfileInputSchema;
};
export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => {
const { user } = ctx;
const userMetadata = handleUserMetadata({ ctx, input });
const locale = input.locale || user.locale;
const emailVerification = await getFeatureFlag(prisma, "email-verification");
const { travelSchedules, ...rest } = input;
const secondaryEmails = input?.secondaryEmails || [];
delete input.secondaryEmails;
const data: Prisma.UserUpdateInput = {
...rest,
metadata: userMetadata,
secondaryEmails: undefined,
};
let isPremiumUsername = false;
const layoutError = validateBookerLayouts(input?.metadata?.defaultBookerLayouts || null);
if (layoutError) {
const t = await getTranslation(locale, "common");
throw new TRPCError({ code: "BAD_REQUEST", message: t(layoutError) });
}
if (input.username && !user.organizationId) {
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) {
const t = await getTranslation(locale, "common");
throw new TRPCError({ code: "BAD_REQUEST", message: t("username_already_taken") });
}
}
} else if (input.username && user.organizationId && user.movedToProfileId) {
// don't change user.username if we have profile.username
delete data.username;
}
if (isPremiumUsername) {
const stripeCustomerId = userMetadata?.stripeCustomerId;
const isPremium = userMetadata?.isPremium;
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.id === getPremiumMonthlyPlanPriceId() &&
subscription.status === "active"
);
if (!isPremiumUsernameSubscriptionActive) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You need to pay for premium username",
});
}
}
const hasEmailBeenChanged = data.email && user.email !== data.email;
let secondaryEmail:
| {
id: number;
emailVerified: Date | null;
}
| null
| undefined;
const primaryEmailVerified = user.emailVerified;
if (hasEmailBeenChanged) {
secondaryEmail = await prisma.secondaryEmail.findUnique({
where: {
email: input.email,
userId: user.id,
},
select: {
id: true,
emailVerified: true,
},
});
if (emailVerification) {
if (secondaryEmail?.emailVerified) {
data.emailVerified = secondaryEmail.emailVerified;
} else {
// Set metadata of the user so we can set it to this updated email once it is confirmed
data.metadata = {
...userMetadata,
emailChangeWaitingForVerification: input.email?.toLocaleLowerCase(),
};
// Check to ensure this email isnt in use
// Don't include email in the data payload if we need to verify
delete data.email;
}
} else {
log.warn("Profile Update - Email verification is disabled - Skipping");
data.emailVerified = null;
}
}
// if defined AND a base 64 string, upload and update the avatar URL
if (input.avatarUrl && input.avatarUrl.startsWith("data:image/png;base64,")) {
data.avatarUrl = await uploadAvatar({
avatar: await resizeBase64Image(input.avatarUrl),
userId: user.id,
});
}
if (input.completedOnboarding) {
const userTeams = await prisma.user.findFirst({
where: {
id: user.id,
},
select: {
teams: {
select: {
id: true,
},
},
},
});
if (userTeams && userTeams.teams.length > 0) {
await Promise.all(
userTeams.teams.map(async (team) => {
await updateNewTeamMemberEventTypes(user.id, team.id);
})
);
}
}
if (travelSchedules) {
const existingSchedules = await prisma.travelSchedule.findMany({
where: {
userId: user.id,
},
});
const schedulesToDelete = existingSchedules.filter(
(schedule) =>
!travelSchedules || !travelSchedules.find((scheduleInput) => scheduleInput.id === schedule.id)
);
await prisma.travelSchedule.deleteMany({
where: {
userId: user.id,
id: {
in: schedulesToDelete.map((schedule) => schedule.id) as number[],
},
},
});
await prisma.travelSchedule.createMany({
data: travelSchedules
.filter((schedule) => !schedule.id)
.map((schedule) => {
return {
userId: user.id,
startDate: schedule.startDate,
endDate: schedule.endDate,
timeZone: schedule.timeZone,
};
}),
});
}
const updatedUserSelect = Prisma.validator<Prisma.UserDefaultArgs>()({
select: {
id: true,
username: true,
email: true,
identityProvider: true,
identityProviderId: true,
metadata: true,
name: true,
createdDate: true,
avatarUrl: true,
locale: true,
schedules: {
select: {
id: true,
},
},
},
});
let updatedUser: Prisma.UserGetPayload<typeof updatedUserSelect>;
try {
updatedUser = await prisma.user.update({
where: {
id: user.id,
},
data,
...updatedUserSelect,
});
} catch (e) {
// Catch unique constraint failure on email field.
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
const meta = e.meta as { target: string[] };
if (meta.target.indexOf("email") !== -1) {
throw new HttpError({ statusCode: 409, message: "email_already_used" });
}
}
throw e; // make sure other errors are rethrown
}
if (user.timeZone !== data.timeZone && updatedUser.schedules.length > 0) {
// on timezone change update timezone of default schedule
const defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
if (!user.defaultScheduleId) {
// set default schedule if not already set
await prisma.user.update({
where: {
id: user.id,
},
data: {
defaultScheduleId,
},
});
}
await prisma.schedule.updateMany({
where: {
id: defaultScheduleId,
},
data: {
timeZone: data.timeZone,
},
});
}
// 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,
},
});
}
if (updatedUser && hasEmailBeenChanged) {
// Skip sending verification email when user tries to change his primary email to a verified secondary email
if (secondaryEmail?.emailVerified) {
secondaryEmails.push({
id: secondaryEmail.id,
email: user.email,
isDeleted: false,
});
} else {
await sendChangeOfEmailVerification({
user: {
username: updatedUser.username ?? "Nameless User",
emailFrom: user.email,
// We know email has been changed here so we can use input
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
emailTo: input.email!,
},
});
}
}
if (secondaryEmails.length) {
const recordsToDelete = secondaryEmails
.filter((secondaryEmail) => secondaryEmail.isDeleted)
.map((secondaryEmail) => secondaryEmail.id);
if (recordsToDelete.length) {
await prisma.secondaryEmail.deleteMany({
where: {
id: {
in: recordsToDelete,
},
userId: updatedUser.id,
},
});
}
const modifiedRecords = secondaryEmails.filter((secondaryEmail) => !secondaryEmail.isDeleted);
if (modifiedRecords.length) {
const secondaryEmailsFromDB = await prisma.secondaryEmail.findMany({
where: {
id: {
in: secondaryEmails.map((secondaryEmail) => secondaryEmail.id),
},
userId: updatedUser.id,
},
});
const keyedSecondaryEmailsFromDB = keyBy(secondaryEmailsFromDB, "id");
const recordsToModifyQueue = modifiedRecords.map((updated) => {
let emailVerified = keyedSecondaryEmailsFromDB[updated.id].emailVerified;
if (secondaryEmail?.id === updated.id) {
emailVerified = primaryEmailVerified;
} else if (updated.email !== keyedSecondaryEmailsFromDB[updated.id].email) {
emailVerified = null;
}
return prisma.secondaryEmail.update({
where: {
id: updated.id,
userId: updatedUser.id,
},
data: {
email: updated.email,
emailVerified,
},
});
});
await prisma.$transaction(recordsToModifyQueue);
}
}
return {
...input,
email: emailVerification && !secondaryEmail?.emailVerified ? user.email : input.email,
avatarUrl: updatedUser.avatarUrl,
hasEmailBeenChanged,
sendEmailVerification: emailVerification && !secondaryEmail?.emailVerified,
};
};
const cleanMetadataAllowedUpdateKeys = (metadata: TUpdateProfileInputSchema["metadata"]) => {
if (!metadata) {
return {};
}
const cleanedMetadata = updateUserMetadataAllowedKeys.safeParse(metadata);
if (!cleanedMetadata.success) {
logger.error("Error cleaning metadata", cleanedMetadata.error);
return {};
}
return cleanedMetadata.data;
};
const handleUserMetadata = ({ ctx, input }: UpdateProfileOptions) => {
const { user } = ctx;
const cleanMetadata = cleanMetadataAllowedUpdateKeys(input.metadata);
const userMetadata = userMetadataSchema.parse(user.metadata);
// Required so we don't override and delete saved values
return { ...userMetadata, ...cleanMetadata };
};

View File

@@ -0,0 +1,53 @@
import { z } from "zod";
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { bookerLayouts, userMetadata } from "@calcom/prisma/zod-utils";
export const updateUserMetadataAllowedKeys = z.object({
sessionTimeout: z.number().optional(), // Minutes
defaultBookerLayouts: bookerLayouts.optional(),
});
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(),
avatarUrl: z.string().nullable().optional(),
timeZone: z.string().optional(),
weekStart: z.string().optional(),
hideBranding: z.boolean().optional(),
allowDynamicBooking: z.boolean().optional(),
allowSEOIndexing: z.boolean().optional(),
receiveMonthlyDigestEmail: z.boolean().optional(),
brandColor: z.string().optional(),
darkBrandColor: z.string().optional(),
theme: z.string().optional().nullable(),
appTheme: z.string().optional().nullable(),
completedOnboarding: z.boolean().optional(),
locale: z.string().optional(),
timeFormat: z.number().optional(),
disableImpersonation: z.boolean().optional(),
metadata: userMetadata.optional(),
travelSchedules: z
.array(
z.object({
id: z.number().optional(),
timeZone: z.string(),
endDate: z.date().optional(),
startDate: z.date(),
})
)
.optional(),
secondaryEmails: z
.array(
z.object({
id: z.number(),
email: z.string(),
isDeleted: z.boolean().default(false),
})
)
.optional(),
});
export type TUpdateProfileInputSchema = z.infer<typeof ZUpdateProfileInputSchema>;

View File

@@ -0,0 +1,60 @@
import z from "zod";
import getApps from "@calcom/app-store/utils";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
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 = await getUsersCredentials(ctx.user);
const foundApp = getApps(credentials, true).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;
};

View File

@@ -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
>;

View File

@@ -0,0 +1,177 @@
import type { TFormSchema } from "@calcom/app-store/routing-forms/trpc/forms.schema";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { prisma } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { entries } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TWorkflowOrderInputSchema } from "./workflowOrder.schema";
type RoutingFormOrderOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TWorkflowOrderInputSchema;
};
export const workflowOrderHandler = async ({ ctx, input }: RoutingFormOrderOptions) => {
const { user } = ctx;
const { include: includedFields } = Prisma.validator<Prisma.WorkflowDefaultArgs>()({
include: {
activeOn: {
select: {
eventType: {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},
},
steps: true,
team: {
select: {
id: true,
slug: true,
name: true,
members: true,
logoUrl: true,
},
},
},
});
const allWorkflows = await prisma.workflow.findMany({
where: {
OR: [
{
userId: user.id,
},
{
team: {
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
},
],
},
include: includedFields,
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
});
const allWorkflowIds = new Set(allWorkflows.map((workflow) => workflow.id));
if (input.ids.some((id) => !allWorkflowIds.has(id))) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
await Promise.all(
input.ids.reverse().map((id, position) => {
return prisma.workflow.update({
where: {
id: id,
},
data: {
position,
},
});
})
);
};
type SupportedFilters = Omit<NonNullable<NonNullable<TFormSchema>["filters"]>, "upIds"> | undefined;
export function getPrismaWhereFromFilters(
user: {
id: number;
},
filters: SupportedFilters
) {
const where = {
OR: [] as Prisma.App_RoutingForms_FormWhereInput[],
};
const prismaQueries: Record<
keyof NonNullable<typeof filters>,
(...args: [number[]]) => Prisma.App_RoutingForms_FormWhereInput
> & {
all: () => Prisma.App_RoutingForms_FormWhereInput;
} = {
userIds: (userIds: number[]) => ({
userId: {
in: userIds,
},
teamId: null,
}),
teamIds: (teamIds: number[]) => ({
team: {
id: {
in: teamIds ?? [],
},
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
}),
all: () => ({
OR: [
{
userId: user.id,
},
{
team: {
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
},
],
}),
};
if (!filters || !hasFilter(filters)) {
where.OR.push(prismaQueries.all());
} else {
for (const entry of entries(filters)) {
if (!entry) {
continue;
}
const [filterName, filter] = entry;
const getPrismaQuery = prismaQueries[filterName];
// filter might be accidentally set undefined as well
if (!getPrismaQuery || !filter) {
continue;
}
where.OR.push(getPrismaQuery(filter));
}
}
return where;
}

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZWorkflowOrderInputSchema = z.object({
ids: z.array(z.number()),
});
export type TWorkflowOrderInputSchema = z.infer<typeof ZWorkflowOrderInputSchema>;

View File

@@ -0,0 +1,70 @@
import publicProcedure from "../../procedures/publicProcedure";
import { importHandler, router } from "../../trpc";
import { slotsRouter } from "../viewer/slots/_router";
import { ZUserEmailVerificationRequiredSchema } from "./checkIfUserEmailVerificationRequired.schema";
import { i18nInputSchema } from "./i18n.schema";
import { ZNoShowInputSchema } from "./noShow.schema";
import { event } from "./procedures/event";
import { session } from "./procedures/session";
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
import { ZSubmitRatingInputSchema } from "./submitRating.schema";
const NAMESPACE = "publicViewer";
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
// things that unauthenticated users can query about themselves
export const publicViewerRouter = router({
session,
i18n: publicProcedure.input(i18nInputSchema).query(async (opts) => {
const handler = await importHandler(namespaced("i18n"), () => import("./i18n.handler"));
return handler(opts);
}),
countryCode: publicProcedure.query(async (opts) => {
const handler = await importHandler(namespaced("countryCode"), () => import("./countryCode.handler"));
return handler(opts);
}),
submitRating: publicProcedure.input(ZSubmitRatingInputSchema).mutation(async (opts) => {
const handler = await importHandler(namespaced("submitRating"), () => import("./submitRating.handler"));
return handler(opts);
}),
noShow: publicProcedure.input(ZNoShowInputSchema).mutation(async (opts) => {
const handler = await importHandler(namespaced("noShow"), () => import("./noShow.handler"));
return handler(opts);
}),
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async (opts) => {
const handler = await importHandler(
namespaced("samlTenantProduct"),
() => import("./samlTenantProduct.handler")
);
return handler(opts);
}),
stripeCheckoutSession: publicProcedure.input(ZStripeCheckoutSessionInputSchema).query(async (opts) => {
const handler = await importHandler(
namespaced("stripeCheckoutSession"),
() => import("./stripeCheckoutSession.handler")
);
return handler(opts);
}),
// REVIEW: This router is part of both the public and private viewer router?
slots: slotsRouter,
event,
ssoConnections: publicProcedure.query(async () => {
const handler = await importHandler(
namespaced("ssoConnections"),
() => import("./ssoConnections.handler")
);
return handler();
}),
checkIfUserEmailVerificationRequired: publicProcedure
.input(ZUserEmailVerificationRequiredSchema)
.query(async (opts) => {
const handler = await importHandler(
namespaced("checkIfUserEmailVerificationRequired"),
() => import("./checkIfUserEmailVerificationRequired.handler")
);
return handler(opts);
}),
});

View File

@@ -0,0 +1,27 @@
import { extractBaseEmail } from "@calcom/lib/extract-base-email";
import logger from "@calcom/lib/logger";
import type { TUserEmailVerificationRequiredSchema } from "./checkIfUserEmailVerificationRequired.schema";
const log = logger.getSubLogger({ prefix: ["checkIfUserEmailVerificationRequired"] });
export const userWithEmailHandler = async ({ input }: { input: TUserEmailVerificationRequiredSchema }) => {
const { userSessionEmail, email } = input;
const baseEmail = extractBaseEmail(email);
const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
? process.env.BLACKLISTED_GUEST_EMAILS.split(",")
: [];
const blacklistedEmail = blacklistedGuestEmails.find(
(guestEmail: string) => guestEmail.toLowerCase() === baseEmail.toLowerCase()
);
if (!!blacklistedEmail && blacklistedEmail !== userSessionEmail) {
log.warn(`blacklistedEmail: ${blacklistedEmail}`);
return true;
}
return false;
};
export default userWithEmailHandler;

View File

@@ -0,0 +1,8 @@
import { z } from "zod";
export const ZUserEmailVerificationRequiredSchema = z.object({
userSessionEmail: z.string().optional(),
email: z.string(),
});
export type TUserEmailVerificationRequiredSchema = z.infer<typeof ZUserEmailVerificationRequiredSchema>;

View File

@@ -0,0 +1,14 @@
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 };
};
export default countryCodeHandler;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,23 @@
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import type { PrismaClient } from "@calcom/prisma";
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,
input.isTeamEvent,
input.org,
ctx.prisma,
input.fromRedirectOfNonOrgLink
);
return event;
};
export default eventHandler;

View File

@@ -0,0 +1,15 @@
import z from "zod";
export const ZEventInputSchema = z.object({
username: z.string(),
eventSlug: z.string(),
isTeamEvent: z.boolean().optional(),
org: z.string().nullable(),
/**
* Informs that the event request has been sent from a page that was reached by a redirect from non-org link(i.e. app.cal.com/username redirected to acme.cal.com/username)
* Based on this decision like whether to allow unpublished organization's event to be served or not can be made.
*/
fromRedirectOfNonOrgLink: z.boolean().optional().default(false),
});
export type TEventInputSchema = z.infer<typeof ZEventInputSchema>;

View File

@@ -0,0 +1,18 @@
import type { I18nInputSchema } from "./i18n.schema";
type I18nOptions = {
input: I18nInputSchema;
};
export const i18nHandler = async ({ input }: I18nOptions) => {
const { locale } = input;
const { serverSideTranslations } = await import("next-i18next/serverSideTranslations");
const i18n = await serverSideTranslations(locale, ["common", "vital"]);
return {
i18n,
locale,
};
};
export default i18nHandler;

View File

@@ -0,0 +1,15 @@
import { lookup } from "bcp-47-match";
import { z } from "zod";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { i18n } = require("@calcom/config/next-i18next.config");
export const i18nInputSchema = z.object({
locale: z
.string()
.min(2)
.transform((locale) => lookup(i18n.locales, locale) || locale),
CalComVersion: z.string(),
});
export type I18nInputSchema = z.infer<typeof i18nInputSchema>;

View File

@@ -0,0 +1,15 @@
import handleMarkNoShow from "@calcom/features/handleMarkNoShow";
import type { TNoShowInputSchema } from "./noShow.schema";
type NoShowOptions = {
input: TNoShowInputSchema;
};
export const noShowHandler = async ({ input }: NoShowOptions) => {
const { bookingUid, attendees, noShowHost } = input;
return handleMarkNoShow({ bookingUid, attendees, noShowHost });
};
export default noShowHandler;

View File

@@ -0,0 +1,26 @@
import { z } from "zod";
export const ZNoShowInputSchema = z
.object({
bookingUid: z.string(),
attendees: z
.array(
z.object({
email: z.string(),
noShow: z.boolean(),
})
)
.optional(),
noShowHost: z.boolean().optional(),
})
.refine(
(data) => {
return (data.attendees && data.attendees.length > 0) || data.noShowHost !== undefined;
},
{
message: "At least one of 'attendees' or 'noShowHost' must be provided",
path: ["attendees", "noShowHost"],
}
);
export type TNoShowInputSchema = z.infer<typeof ZNoShowInputSchema>;

View File

@@ -0,0 +1,11 @@
import publicProcedure from "../../../procedures/publicProcedure";
import { importHandler } from "../../../trpc";
import { ZEventInputSchema } from "../event.schema";
const NAMESPACE = "publicViewer";
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
export const event = publicProcedure.input(ZEventInputSchema).query(async (opts) => {
const handler = await importHandler(namespaced("event"), () => import("../event.handler"));
return handler(opts);
});

View File

@@ -0,0 +1,12 @@
import sessionMiddleware from "../../../middlewares/sessionMiddleware";
import publicProcedure from "../../../procedures/publicProcedure";
import { importHandler } from "../../../trpc";
const NAMESPACE = "publicViewer";
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
export const session = publicProcedure.use(sessionMiddleware).query(async (opts) => {
const handler = await importHandler(namespaced("session"), () => import("../session.handler"));
return handler(opts);
});

View File

@@ -0,0 +1,20 @@
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
import type { PrismaClient } from "@calcom/prisma";
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
type SamlTenantProductOptions = {
ctx: {
prisma: PrismaClient;
};
input: TSamlTenantProductInputSchema;
};
export const samlTenantProductHandler = ({ ctx, input }: SamlTenantProductOptions) => {
const { prisma } = ctx;
const { email } = input;
return ssoTenantProduct(prisma, email);
};
export default samlTenantProductHandler;

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZSamlTenantProductInputSchema = z.object({
email: z.string().email(),
});
export type TSamlTenantProductInputSchema = z.infer<typeof ZSamlTenantProductInputSchema>;

View File

@@ -0,0 +1,13 @@
import type { Session } from "next-auth";
type SessionOptions = {
ctx: {
session: Session | null;
};
};
export const sessionHandler = async ({ ctx }: SessionOptions) => {
return ctx.session;
};
export default sessionHandler;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,31 @@
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { TRPCError } from "@trpc/server";
export const handler = async () => {
try {
if (HOSTED_CAL_FEATURES || !isSAMLLoginEnabled) {
return {
connectionExists: null,
};
}
const { connectionController } = await jackson();
const connections = await connectionController.getConnections({
tenant: samlTenantID,
product: samlProductID,
});
return {
connectionExists: connections.length > 0,
};
} catch (err) {
console.error("Error getting SSO connections", err);
throw new TRPCError({ code: "BAD_REQUEST", message: "Fetching SSO connections failed." });
}
};
export default handler;

Some files were not shown because too many files have changed in this diff Show More