Skip to content

Commit

Permalink
Removes the getUser logic from createContext, instead use isAuthed (#…
Browse files Browse the repository at this point in the history
…7902)

* Removes the getUser logic from createContext, instead use isAuthed

* Fix types :party
  • Loading branch information
emrysal authored Mar 23, 2023
1 parent fa17139 commit e4893c2
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 136 deletions.
1 change: 0 additions & 1 deletion apps/web/server/lib/ssg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
ctx: {
prisma,
session: null,
user: null,
locale,
i18n: _i18n,
},
Expand Down
116 changes: 19 additions & 97 deletions packages/trpc/server/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,108 +3,32 @@ import type { Session } from "next-auth";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
import { getLocaleFromHeaders } from "@calcom/lib/i18n";
import prisma from "@calcom/prisma";
import type { SelectedCalendar, User as PrismaUser, Credential } from "@calcom/prisma/client";

import type { Maybe } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";

type CreateContextOptions = CreateNextContextOptions | GetServerSidePropsContext;

async function getUserFromSession({
session,
req,
}: {
session: Maybe<Session>;
req: CreateContextOptions["req"];
}) {
if (!session?.user?.id) {
return null;
}

const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
timeZone: true,
weekStart: true,
startTime: true,
endTime: true,
defaultScheduleId: true,
bufferTime: true,
theme: true,
createdDate: true,
hideBranding: true,
avatar: true,
twoFactorEnabled: true,
disableImpersonation: true,
identityProvider: true,
brandColor: true,
darkBrandColor: true,
away: true,
credentials: {
select: {
id: true,
type: true,
key: true,
userId: true,
appId: true,
invalid: true,
},
orderBy: {
id: "asc",
},
},
selectedCalendars: {
select: {
externalId: true,
integration: true,
},
},
completedOnboarding: true,
destinationCalendar: true,
locale: true,
timeFormat: true,
trialEndsAt: true,
metadata: true,
role: true,
},
});

// some hacks to make sure `username` and `email` are never inferred as `null`
if (!user) {
return null;
}
const { email, username } = user;
if (!email) {
return null;
}
const rawAvatar = user.avatar;
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
user.avatar = rawAvatar ? `${WEBAPP_URL}/${user.username}/avatar.png` : defaultAvatarSrc({ email });

const locale = user.locale || getLocaleFromHeaders(req);
return {
...user,
rawAvatar,
email,
username,
locale,
};
}

type CreateInnerContextOptions = {
session: Session | null;
locale: string;
user: Awaited<ReturnType<typeof getUserFromSession>>;
user?: Omit<
PrismaUser,
| "locale"
| "twoFactorSecret"
| "emailVerified"
| "password"
| "identityProviderId"
| "invitedTo"
| "allowDynamicBooking"
| "verified"
> & {
locale: NonNullable<PrismaUser["locale"]>;
credentials?: Credential[];
selectedCalendars?: Partial<SelectedCalendar>[];
};
i18n: Awaited<ReturnType<typeof serverSideTranslations>>;
} & Partial<CreateContextOptions>;

Expand Down Expand Up @@ -144,11 +68,9 @@ export const createContext = async (
// for API-response caching see https://trpc.io/docs/caching
const session = await sessionGetter({ req, res });

const user = await getUserFromSession({ session, req });
const locale = user?.locale ?? getLocaleFromHeaders(req);
const i18n = await serverSideTranslations(locale, ["common", "vital"]);

const contextInner = await createContextInner({ session, i18n, locale, user });
const locale = getLocaleFromHeaders(req);
const i18n = await serverSideTranslations(getLocaleFromHeaders(req), ["common", "vital"]);
const contextInner = await createContextInner({ session, i18n, locale });
return {
...contextInner,
req,
Expand Down
140 changes: 102 additions & 38 deletions packages/trpc/server/trpc.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,96 @@
import type { Session } from "next-auth";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import superjson from "superjson";

import rateLimit from "@calcom/lib/rateLimit";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";

import type { Maybe } from "@trpc/server";
import { initTRPC, TRPCError } from "@trpc/server";

import type { createContextInner } from "./createContext";

async function getUserFromSession({ session }: { session: Maybe<Session> }) {
if (!session?.user?.id) {
return null;
}

const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
timeZone: true,
weekStart: true,
startTime: true,
endTime: true,
defaultScheduleId: true,
bufferTime: true,
theme: true,
createdDate: true,
hideBranding: true,
avatar: true,
twoFactorEnabled: true,
disableImpersonation: true,
identityProvider: true,
brandColor: true,
darkBrandColor: true,
away: true,
credentials: {
select: {
id: true,
type: true,
key: true,
userId: true,
appId: true,
invalid: true,
},
orderBy: {
id: "asc",
},
},
selectedCalendars: {
select: {
externalId: true,
integration: true,
},
},
completedOnboarding: true,
destinationCalendar: true,
locale: true,
timeFormat: true,
trialEndsAt: true,
metadata: true,
role: true,
},
});

// some hacks to make sure `username` and `email` are never inferred as `null`
if (!user) {
return null;
}
const { email, username } = user;
if (!email) {
return null;
}
const rawAvatar = user.avatar;
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
user.avatar = rawAvatar ? `${WEBAPP_URL}/${user.username}/avatar.png` : defaultAvatarSrc({ email });

return {
...user,
rawAvatar,
email,
username,
};
}

const t = initTRPC.context<typeof createContextInner>().create({
transformer: superjson,
});
Expand All @@ -18,20 +103,30 @@ const perfMiddleware = t.middleware(async ({ path, type, next }) => {
return result;
});

const isAuthedMiddleware = t.middleware(({ ctx, next }) => {
if (!ctx.user || !ctx.session) {
const isAuthed = t.middleware(async ({ ctx: { session, locale, ...ctx }, next }) => {
const user = await getUserFromSession({ session });
if (!user || !session) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const i18n =
user.locale && user.locale !== locale
? await serverSideTranslations(user.locale, ["common", "vital"])
: ctx.i18n;
locale = user.locale || locale;
return next({
ctx: {
i18n,
// infers that `user` and `session` are non-nullable to downstream procedures
session: ctx.session,
user: ctx.user,
session,
user: {
...user,
locale,
},
},
});
});

const isAdminMiddleware = isAuthedMiddleware.unstable_pipe(({ ctx, next }) => {
const isAdminMiddleware = isAuthed.unstable_pipe(({ ctx, next }) => {
if (ctx.user.role !== "ADMIN") {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
Expand All @@ -40,41 +135,10 @@ const isAdminMiddleware = isAuthedMiddleware.unstable_pipe(({ ctx, next }) => {
});
});

interface IRateLimitOptions {
intervalInMs: number;
limit: number;
}
const isRateLimitedByUserIdMiddleware = ({ intervalInMs, limit }: IRateLimitOptions) =>
t.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: {
// infers that `user` and `session` are non-nullable to downstream procedures
session: ctx.session,
user: ctx.user,
},
});
});

export const router = t.router;
export const mergeRouters = t.mergeRouters;
export const middleware = t.middleware;
export const publicProcedure = t.procedure.use(perfMiddleware);
export const authedProcedure = t.procedure.use(perfMiddleware).use(isAuthedMiddleware);
export const authedRateLimitedProcedure = ({ intervalInMs, limit }: IRateLimitOptions) =>
t.procedure
.use(perfMiddleware)
.use(isAuthedMiddleware)
.use(isRateLimitedByUserIdMiddleware({ intervalInMs, limit }));
export const authedProcedure = t.procedure.use(perfMiddleware).use(isAuthed);

export const authedAdminProcedure = t.procedure.use(perfMiddleware).use(isAdminMiddleware);

1 comment on commit e4893c2

@vercel
Copy link

@vercel vercel bot commented on e4893c2 Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ui – ./apps/storybook

cal-com-storybook.vercel.app
ui-git-main-cal.vercel.app
ui.cal.com
timelessui.com
ui-cal.vercel.app
www.timelessui.com

Please sign in to comment.