From e4893c2218e319b139caa553d588fd9d93902899 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Thu, 23 Mar 2023 19:46:33 +0100 Subject: [PATCH] Removes the getUser logic from createContext, instead use isAuthed (#7902) * Removes the getUser logic from createContext, instead use isAuthed * Fix types :party --- apps/web/server/lib/ssg.ts | 1 - packages/trpc/server/createContext.ts | 116 ++++----------------- packages/trpc/server/trpc.ts | 140 +++++++++++++++++++------- 3 files changed, 121 insertions(+), 136 deletions(-) diff --git a/apps/web/server/lib/ssg.ts b/apps/web/server/lib/ssg.ts index c21f5f3b2d..75f031efef 100644 --- a/apps/web/server/lib/ssg.ts +++ b/apps/web/server/lib/ssg.ts @@ -29,7 +29,6 @@ export async function ssgInit(opts: GetStat ctx: { prisma, session: null, - user: null, locale, i18n: _i18n, }, diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 1de2931810..bc3a7fc0d9 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -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; - 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>; + user?: Omit< + PrismaUser, + | "locale" + | "twoFactorSecret" + | "emailVerified" + | "password" + | "identityProviderId" + | "invitedTo" + | "allowDynamicBooking" + | "verified" + > & { + locale: NonNullable; + credentials?: Credential[]; + selectedCalendars?: Partial[]; + }; i18n: Awaited>; } & Partial; @@ -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, diff --git a/packages/trpc/server/trpc.ts b/packages/trpc/server/trpc.ts index 6577b51d97..be54d719d1 100644 --- a/packages/trpc/server/trpc.ts +++ b/packages/trpc/server/trpc.ts @@ -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 }) { + 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().create({ transformer: superjson, }); @@ -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" }); } @@ -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);