From f0db97bbb1f97aeed4764f7fbf43471bc69cd0c2 Mon Sep 17 00:00:00 2001 From: Nafees Nazik <84864519+G3root@users.noreply.github.com> Date: Thu, 9 Feb 2023 06:42:45 +0530 Subject: [PATCH] feat: return `x-vercel-ip-timezone` in headers (#6849) * feat: add trpc to matcher and pass vercel timezone header * feat: pass request to context * feat: return timezone in header * refactor: split context * fix: remove tsignore comment * Update [trpc].ts --------- Co-authored-by: zomars --- apps/web/middleware.ts | 11 ++++++++ apps/web/pages/api/trpc/[trpc].ts | 30 +++++++++++++--------- packages/trpc/server/createContext.ts | 37 ++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 6079138d4e..143445d58b 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -59,6 +59,16 @@ const middleware: NextMiddleware = async (req) => { return NextResponse.rewrite(url); } + if (url.pathname.startsWith("/api/trpc/")) { + const requestHeaders = new Headers(req.headers); + requestHeaders.set("x-cal-timezone", req.headers.get("x-vercel-ip-timezone") ?? ""); + return NextResponse.next({ + request: { + headers: requestHeaders, + }, + }); + } + if (url.pathname.startsWith("/auth/login")) { const moreHeaders = new Headers(req.headers); // Use this header to actually enforce CSP, otherwise it is running in Report Only mode on all pages. @@ -79,6 +89,7 @@ export const config = { "/api/auth/:path*", "/apps/routing_forms/:path*", "/:path*/embed", + "/api/trpc/:path*", "/auth/login", ], }; diff --git a/apps/web/pages/api/trpc/[trpc].ts b/apps/web/pages/api/trpc/[trpc].ts index 4da664fc2b..ea7f7a7786 100644 --- a/apps/web/pages/api/trpc/[trpc].ts +++ b/apps/web/pages/api/trpc/[trpc].ts @@ -1,6 +1,8 @@ /** * This file contains tRPC's HTTP response handler */ +import { z } from "zod"; + import * as trpcNext from "@calcom/trpc/server/adapters/next"; import { createContext } from "@calcom/trpc/server/createContext"; import { appRouter } from "@calcom/trpc/server/routers/_app"; @@ -35,29 +37,35 @@ export default trpcNext.createNextApiHandler({ const TWO_HOURS_IN_SECONDS = 60 * 60 * 2; // Our response to indicate no caching - const noCacheResponse = {}; + const defaultHeaders: Record<"headers", Record> = { + headers: {}, + }; + + if (!!ctx?.req) { + const timezone = z.string().safeParse(ctx.req.headers["x-vercel-ip-timezone"]); + if (timezone.success) defaultHeaders.headers["x-cal-timezone"] = timezone.data; + } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ctx.req is available for SSR but not SSG - const isSSR = !!ctx?.req; - if (isSSR) { - return noCacheResponse; + if (!!ctx?.req) { + return defaultHeaders; } // No caching if we have a non-public path // Assuming we have all our public routes in `viewer.public` if (!paths || !paths.every((path) => path.startsWith("viewer.public."))) { - return noCacheResponse; + return defaultHeaders; } // No caching if we have any procedures errored if (errors.length !== 0) { - return noCacheResponse; + return defaultHeaders; } // Never cache non-queries (aka mutations) if (type !== "query") { - return noCacheResponse; + return defaultHeaders; } // cache request for 1 day + revalidate once every 5 seconds @@ -77,11 +85,9 @@ export default trpcNext.createNextApiHandler({ // Get the cache value of the matching element, if any if (matchedPath) cacheValue = cacheRules[matchedPath]; + if (cacheValue) defaultHeaders.headers["Cache-Control"] = cacheValue; + // Finally we respond with our resolved cache value - return { - headers: { - "Cache-Control": cacheValue, - }, - }; + return defaultHeaders; }, }); diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 0498ee2e78..5cb8057f77 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -101,24 +101,47 @@ async function getUserFromSession({ }; } +type CreateInnerContextOptions = { + session: Session | null; + locale: string; + user: Awaited>; + i18n: Awaited>; +} & Partial; + +/** + * 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 `createSSGHelpers` where we don't have `req`/`res` + * + * @see https://trpc.io/docs/context#inner-and-outer-context + */ +export async function createContextInner(opts: CreateInnerContextOptions) { + return { + prisma, + ...opts, + }; +} + /** * Creates context for an incoming request * @link https://trpc.io/docs/context */ -export const createContext = async ({ req }: CreateContextOptions, sessionGetter = getSession) => { +export const createContext = async ({ req, res }: CreateContextOptions, sessionGetter = getSession) => { // for API-response caching see https://trpc.io/docs/caching const session = await sessionGetter({ req }); 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 }); return { - i18n, - prisma, - session, - user, - locale, + ...contextInner, + req, + res, }; }; -export type Context = trpc.inferAsyncReturnType; +export type Context = trpc.inferAsyncReturnType;