From e4b766b9d60bd0bb5f06e14ebecd05622c452cf1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 28 Feb 2023 22:40:19 +0100 Subject: [PATCH] refactor: cleanup use of rawInput and pipe mdwr in trpc router (#7422) * cleanup use of rawInput and pipe mdwr * Update ssg.ts * Update ssg.ts * Update viewer.tsx --------- Co-authored-by: zomars --- apps/web/server/lib/ssg.ts | 1 + packages/config/eslint-preset.js | 19 ++- packages/trpc/package.json | 12 +- packages/trpc/server/createContext.ts | 10 +- .../server/routers/viewer/availability.tsx | 13 ++- .../trpc/server/routers/viewer/eventTypes.ts | 110 +++++++++--------- packages/trpc/server/routers/viewer/slots.tsx | 8 +- packages/trpc/server/routers/viewer/teams.tsx | 2 +- .../trpc/server/routers/viewer/webhook.tsx | 81 ++++++------- .../trpc/server/routers/viewer/workflows.tsx | 2 +- packages/trpc/server/trpc.ts | 16 +-- packages/trpc/tsconfig.json | 3 +- packages/types/next-auth.d.ts | 4 +- yarn.lock | 32 ++--- 14 files changed, 163 insertions(+), 150 deletions(-) diff --git a/apps/web/server/lib/ssg.ts b/apps/web/server/lib/ssg.ts index c21f5f3b2d..4fe41c8c06 100644 --- a/apps/web/server/lib/ssg.ts +++ b/apps/web/server/lib/ssg.ts @@ -26,6 +26,7 @@ export async function ssgInit(opts: GetStat const ssg = createProxySSGHelpers({ router: appRouter, transformer: superjson, + // @ts-expect-error FIXME The signature expects req and res. Which we don't have in an SSG context. ctx: { prisma, session: null, diff --git a/packages/config/eslint-preset.js b/packages/config/eslint-preset.js index 8825b059ed..7b55dc27a3 100644 --- a/packages/config/eslint-preset.js +++ b/packages/config/eslint-preset.js @@ -18,12 +18,17 @@ module.exports = { "jsx-a11y/role-supports-aria-props": "off", // @see https://github.com/vercel/next.js/issues/27989#issuecomment-897638654 "react/jsx-curly-brace-presence": ["error", { props: "never", children: "never" }], "react/self-closing-comp": ["error", { component: true, html: true }], - "@typescript-eslint/no-unused-vars": "off", - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ + "@typescript-eslint/no-unused-vars": [ "warn", - { vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" }, + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + }, ], + "unused-imports/no-unused-imports": "error", }, overrides: [ { @@ -45,16 +50,18 @@ module.exports = { }, overrides: [ { - files: ["playwright/**/*.{tsx,ts}"], + files: ["**/playwright/**/*.{tsx,ts}"], rules: { + "@typescript-eslint/no-unused-vars": "off", "no-undef": "off", }, }, ], }, { - files: ["playwright/**/*.{js,jsx}"], + files: ["**/playwright/**/*.{js,jsx}"], rules: { + "@typescript-eslint/no-unused-vars": "off", "no-undef": "off", }, }, diff --git a/packages/trpc/package.json b/packages/trpc/package.json index f3aa7fa7da..3fd28abf16 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -5,12 +5,16 @@ "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": "^4.3.9", - "@trpc/client": "^10.10.0", - "@trpc/next": "^10.10.0", - "@trpc/react-query": "^10.10.0", - "@trpc/server": "^10.10.0", + "@trpc/client": "^10.13.0", + "@trpc/next": "^10.13.0", + "@trpc/react-query": "^10.13.0", + "@trpc/server": "^10.13.0", "superjson": "1.9.1", "zod": "^3.20.2" } diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 5cb8057f77..5679e46161 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -8,11 +8,10 @@ import { getLocaleFromHeaders } from "@calcom/lib/i18n"; import { defaultAvatarSrc } from "@calcom/lib/profile"; import prisma from "@calcom/prisma"; -import * as trpc from "@trpc/server"; -import { Maybe } from "@trpc/server"; -import * as trpcNext from "@trpc/server/adapters/next"; +import type { Maybe } from "@trpc/server"; +import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; -type CreateContextOptions = trpcNext.CreateNextContextOptions | GetServerSidePropsContext; +type CreateContextOptions = CreateNextContextOptions | GetServerSidePropsContext; async function getUserFromSession({ session, @@ -24,6 +23,7 @@ async function getUserFromSession({ if (!session?.user?.id) { return null; } + const user = await prisma.user.findUnique({ where: { id: session.user.id, @@ -143,5 +143,3 @@ export const createContext = async ({ req, res }: CreateContextOptions, sessionG res, }; }; - -export type Context = trpc.inferAsyncReturnType; diff --git a/packages/trpc/server/routers/viewer/availability.tsx b/packages/trpc/server/routers/viewer/availability.tsx index a69254342d..04d5185da0 100644 --- a/packages/trpc/server/routers/viewer/availability.tsx +++ b/packages/trpc/server/routers/viewer/availability.tsx @@ -1,13 +1,18 @@ -import { Availability as AvailabilityModel, Prisma, Schedule as ScheduleModel, User } from "@prisma/client"; +import type { + Availability as AvailabilityModel, + Prisma, + Schedule as ScheduleModel, + User, +} from "@prisma/client"; import { z } from "zod"; import { getUserAvailability } from "@calcom/core/getUserAvailability"; import dayjs from "@calcom/dayjs"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule, getWorkingHours } from "@calcom/lib/availability"; import { yyyymmdd } from "@calcom/lib/date-fns"; -import { PrismaClient } from "@calcom/prisma/client"; +import type { PrismaClient } from "@calcom/prisma/client"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; -import { Schedule, TimeRange } from "@calcom/types/schedule"; +import type { Schedule, TimeRange } from "@calcom/types/schedule"; import { TRPCError } from "@trpc/server"; @@ -464,7 +469,7 @@ const setupDefaultSchedule = async (userId: number, scheduleId: number, prisma: }); }; -const isDefaultSchedule = (scheduleId: number, user: Partial) => { +const _isDefaultSchedule = (scheduleId: number, user: Partial) => { return !user.defaultScheduleId || user.defaultScheduleId === scheduleId; }; diff --git a/packages/trpc/server/routers/viewer/eventTypes.ts b/packages/trpc/server/routers/viewer/eventTypes.ts index 08b14f5c4d..a31ae3901c 100644 --- a/packages/trpc/server/routers/viewer/eventTypes.ts +++ b/packages/trpc/server/routers/viewer/eventTypes.ts @@ -117,62 +117,69 @@ const EventTypeDuplicateInput = z.object({ length: z.number(), }); -const eventOwnerProcedure = authedProcedure.use(async ({ ctx, rawInput, next }) => { - // Prevent non-owners to update/delete a team event - const event = await ctx.prisma.eventType.findUnique({ - where: { id: (rawInput as Record<"id", number>)?.id }, - include: { - users: true, - team: { - select: { - members: { - select: { - userId: true, - role: true, +const eventOwnerProcedure = authedProcedure + .input( + z.object({ + id: z.number(), + users: z.array(z.string()).optional().default([]), + }) + ) + .use(async ({ ctx, input, next }) => { + // Prevent non-owners to update/delete a team event + const event = await ctx.prisma.eventType.findUnique({ + where: { id: input.id }, + include: { + users: true, + team: { + select: { + members: { + select: { + userId: true, + role: true, + }, }, }, }, }, - }, + }); + + if (!event) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + const isAuthorized = (function () { + if (event.team) { + return event.team.members + .filter((member) => member.role === MembershipRole.OWNER || member.role === MembershipRole.ADMIN) + .map((member) => member.userId) + .includes(ctx.user.id); + } + return event.userId === ctx.user.id || event.users.find((user) => user.id === ctx.user.id); + })(); + + if (!isAuthorized) { + console.warn(`User ${ctx.user.id} attempted to an access an event ${event.id} they do not own.`); + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + const isAllowed = (function () { + if (event.team) { + const allTeamMembers = event.team.members.map((member) => member.userId); + return input.users.every((userId: string) => allTeamMembers.includes(Number.parseInt(userId))); + } + return input.users.every((userId: string) => Number.parseInt(userId) === ctx.user.id); + })(); + + if (!isAllowed) { + console.warn( + `User ${ctx.user.id} attempted to an create an event for users ${input.users.join(", ")}.` + ); + throw new TRPCError({ code: "FORBIDDEN" }); + } + + return next(); }); - if (!event) { - throw new TRPCError({ code: "NOT_FOUND" }); - } - - const isAuthorized = (function () { - if (event.team) { - return event.team.members - .filter((member) => member.role === MembershipRole.OWNER || member.role === MembershipRole.ADMIN) - .map((member) => member.userId) - .includes(ctx.user.id); - } - return event.userId === ctx.user.id || event.users.find((user) => user.id === ctx.user.id); - })(); - - if (!isAuthorized) { - console.warn(`User ${ctx.user.id} attempted to an access an event ${event.id} they do not own.`); - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - - const inputUsers = (rawInput as any).users || []; - - const isAllowed = (function () { - if (event.team) { - const allTeamMembers = event.team.members.map((member) => member.userId); - return inputUsers.every((userId: string) => allTeamMembers.includes(Number.parseInt(userId))); - } - return inputUsers.every((userId: string) => Number.parseInt(userId) === ctx.user.id); - })(); - - if (!isAllowed) { - console.warn(`User ${ctx.user.id} attempted to an create an event for users ${inputUsers.join(", ")}.`); - throw new TRPCError({ code: "FORBIDDEN" }); - } - - return next(); -}); - export const eventTypesRouter = router({ // REVIEW: What should we name this procedure? getByViewer: authedProcedure.query(async ({ ctx }) => { @@ -764,9 +771,6 @@ export const eventTypesRouter = router({ webhooks: _webhooks, // eslint-disable-next-line @typescript-eslint/no-unused-vars schedule: _schedule, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - not typed correctly as its set on SSR - descriptionAsSafeHTML: _descriptionAsSafeHTML, ...rest } = eventType; diff --git a/packages/trpc/server/routers/viewer/slots.tsx b/packages/trpc/server/routers/viewer/slots.tsx index 1be63e9029..95256dbe53 100644 --- a/packages/trpc/server/routers/viewer/slots.tsx +++ b/packages/trpc/server/routers/viewer/slots.tsx @@ -4,15 +4,17 @@ import { z } from "zod"; import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours"; import type { CurrentSeats } from "@calcom/core/getUserAvailability"; import { getUserAvailability } from "@calcom/core/getUserAvailability"; -import dayjs, { Dayjs } from "@calcom/dayjs"; +import type { Dayjs } from "@calcom/dayjs"; +import dayjs from "@calcom/dayjs"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds"; import logger from "@calcom/lib/logger"; import { performance } from "@calcom/lib/server/perfObserver"; import getTimeSlots from "@calcom/lib/slots"; -import prisma, { availabilityUserSelect } from "@calcom/prisma"; +import type prisma from "@calcom/prisma"; +import { availabilityUserSelect } from "@calcom/prisma"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; -import { EventBusyDate } from "@calcom/types/Calendar"; +import type { EventBusyDate } from "@calcom/types/Calendar"; import { TRPCError } from "@trpc/server"; diff --git a/packages/trpc/server/routers/viewer/teams.tsx b/packages/trpc/server/routers/viewer/teams.tsx index e9b47ee4af..023d455282 100644 --- a/packages/trpc/server/routers/viewer/teams.tsx +++ b/packages/trpc/server/routers/viewer/teams.tsx @@ -177,7 +177,7 @@ export const viewerTeamsRouter = router({ // If we save slug, we don't need the requestedSlug anymore const metadataParse = teamMetadataSchema.safeParse(prevTeam.metadata); if (metadataParse.success) { - const { requestedSlug, ...cleanMetadata } = metadataParse.data || {}; + const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {}; data.metadata = { ...cleanMetadata, }; diff --git a/packages/trpc/server/routers/viewer/webhook.tsx b/packages/trpc/server/routers/viewer/webhook.tsx index d2d557b192..b06e1344a5 100644 --- a/packages/trpc/server/routers/viewer/webhook.tsx +++ b/packages/trpc/server/routers/viewer/webhook.tsx @@ -1,4 +1,4 @@ -import { Prisma } from "@prisma/client"; +import type { Prisma } from "@prisma/client"; import { v4 } from "uuid"; import { z } from "zod"; @@ -19,60 +19,55 @@ const webhookIdAndEventTypeIdSchema = z.object({ eventTypeId: z.number().optional(), }); -const webhookProcedure = authedProcedure.use(async ({ ctx, rawInput, next }) => { - // Endpoints that just read the logged in user's data - like 'list' don't necessary have any input - if (!rawInput) { - return next(); - } - const webhookIdAndEventTypeId = webhookIdAndEventTypeIdSchema.safeParse(rawInput); - if (!webhookIdAndEventTypeId.success) { - throw new TRPCError({ code: "PARSE_ERROR" }); - } - const { eventTypeId, id } = webhookIdAndEventTypeId.data; +const webhookProcedure = authedProcedure + .input(webhookIdAndEventTypeIdSchema.optional()) + .use(async ({ ctx, input, next }) => { + // Endpoints that just read the logged in user's data - like 'list' don't necessary have any input + if (!input) return next(); + const { eventTypeId, id } = input; - // A webhook is either linked to Event Type or to a user. - if (eventTypeId) { - const team = await ctx.prisma.team.findFirst({ - where: { - eventTypes: { - some: { - id: eventTypeId, + // A webhook is either linked to Event Type or to a user. + if (eventTypeId) { + const team = await ctx.prisma.team.findFirst({ + where: { + eventTypes: { + some: { + id: eventTypeId, + }, }, }, - }, - include: { - members: true, - }, - }); + include: { + members: true, + }, + }); - // Team should be available and the user should be a member of the team - if (!team?.members.some((membership) => membership.userId === ctx.user.id)) { - throw new TRPCError({ - code: "UNAUTHORIZED", + // Team should be available and the user should be a member of the team + if (!team?.members.some((membership) => membership.userId === ctx.user.id)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } + } else if (id) { + const authorizedHook = await ctx.prisma.webhook.findFirst({ + where: { + id: id, + userId: ctx.user.id, + }, }); + if (!authorizedHook) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } } - } else if (id) { - const authorizedHook = await ctx.prisma.webhook.findFirst({ - where: { - id: id, - userId: ctx.user.id, - }, - }); - if (!authorizedHook) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); - } - } - return next(); -}); + return next(); + }); export const webhookRouter = router({ list: webhookProcedure .input( z .object({ - eventTypeId: z.number().optional(), appId: z.string().optional(), }) .optional() diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index c49f17c718..9170595834 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -890,7 +890,7 @@ export const workflowsRouter = router({ if (!userWorkflow.user?.teams.length && isSMSAction(s.action)) { throw new TRPCError({ code: "UNAUTHORIZED" }); } - const { id: stepId, ...stepToAdd } = s; + const { id: _stepId, ...stepToAdd } = s; return stepToAdd; } }); diff --git a/packages/trpc/server/trpc.ts b/packages/trpc/server/trpc.ts index 8d6352dec2..72edfc9897 100644 --- a/packages/trpc/server/trpc.ts +++ b/packages/trpc/server/trpc.ts @@ -1,13 +1,12 @@ import superjson from "superjson"; -import { ZodError } from "zod"; import rateLimit from "@calcom/lib/rateLimit"; import { initTRPC, TRPCError } from "@trpc/server"; -import { Context } from "./createContext"; +import type { createContext } from "./createContext"; -const t = initTRPC.context().create({ +const t = initTRPC.context().create({ transformer: superjson, }); @@ -32,18 +31,15 @@ const isAuthedMiddleware = t.middleware(({ ctx, next }) => { }); }); -const isAdminMiddleware = t.middleware(({ ctx, next }) => { - if (!ctx.user || !ctx.session || ctx.user.role !== "ADMIN") { +const isAdminMiddleware = isAuthedMiddleware.unstable_pipe(({ ctx, next }) => { + if (ctx.user.role !== "ADMIN") { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ - ctx: { - // infers that `user` and `session` are non-nullable to downstream procedures - session: ctx.session, - user: ctx.user, - }, + ctx: { user: ctx.user }, }); }); + interface IRateLimitOptions { intervalInMs: number; limit: number; diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json index 11e86362e1..68793ecb88 100644 --- a/packages/trpc/tsconfig.json +++ b/packages/trpc/tsconfig.json @@ -1,3 +1,4 @@ { - "extends": "@calcom/tsconfig/react-library.json" + "extends": "@calcom/tsconfig/react-library.json", + "include": ["client", "next", "react", "server"] } diff --git a/packages/types/next-auth.d.ts b/packages/types/next-auth.d.ts index 8890fe50c3..ef7beb8e0a 100644 --- a/packages/types/next-auth.d.ts +++ b/packages/types/next-auth.d.ts @@ -1,5 +1,5 @@ -import { User as PrismaUser } from "@prisma/client"; -import { DefaultUser } from "next-auth"; +import type { User as PrismaUser } from "@prisma/client"; +import type { DefaultUser } from "next-auth"; declare module "next-auth" { /** diff --git a/yarn.lock b/yarn.lock index 70d42c0df8..36d2a9f986 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7910,27 +7910,27 @@ javascript-natural-sort "0.7.1" lodash "4.17.21" -"@trpc/client@^10.10.0": - version "10.10.0" - resolved "https://registry.yarnpkg.com/@trpc/client/-/client-10.10.0.tgz#5abdbf399639f2fdfe605682d2cdf3f9dba87cb1" - integrity sha512-HRVGkOsR4FIYpyQILP84HLbj6pRnLKgxy4AIelTf9d9TxD60M5bNhbR2Uz3hqNSb9a2ppaRJBLv7twlV9b4qHQ== +"@trpc/client@^10.13.0": + version "10.13.0" + resolved "https://registry.yarnpkg.com/@trpc/client/-/client-10.13.0.tgz#8999a7ba068a684629071b77a07c00a141585eca" + integrity sha512-r4KuN0os2J194lxg5jn4+o3uNlqunLFYptwTHcVW4Q0XGO0ZoTKLHuxT7c9IeDivkAs6G5oVEPiKhptkag36dQ== -"@trpc/next@^10.10.0": - version "10.10.0" - resolved "https://registry.yarnpkg.com/@trpc/next/-/next-10.10.0.tgz#083ff327b68b005d60b19af07008377f830ad73a" - integrity sha512-7d84L2OoF0RW06drTbNGOOggwMes8JxI3Ln/VOIaYeERzwOFNCtWPmGjWCdq4l1SKbXC6+baS+b9n5cXc+euwA== +"@trpc/next@^10.13.0": + version "10.13.0" + resolved "https://registry.yarnpkg.com/@trpc/next/-/next-10.13.0.tgz#64393f0d8dbfae7d87e54260dea532702bd1ecdf" + integrity sha512-Q4rnuuiSUXDYv34f8FNUKhEMQFgLJTTJean78YjhG3Aaci+r4sew4hPmRvDRut8fBpa+EtExq+dv1EUbzlXgJg== dependencies: react-ssr-prepass "^1.5.0" -"@trpc/react-query@^10.10.0": - version "10.10.0" - resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-10.10.0.tgz#1faf32056aa3ee5ecb2ffd0d15e8abbcd7eff911" - integrity sha512-Jc/uii1MPevf95/z/W3ufYGHvrFvrtkjxQ8UuXhJCzOgv/FGPqhmA5PH124nLHEgGLBA7zQxHumofhdXosEhUQ== +"@trpc/react-query@^10.13.0": + version "10.13.0" + resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-10.13.0.tgz#ca5a1da0ea126976d7435775f2ba1846cb7fad59" + integrity sha512-y4jbojrDFdEl1KBejBoMWIofcUXDHQA8wf01eKMEDV7Jwc7lhq6R1dxYtKzeF+s5wqfnPWFOGZDmB3flzv07Dw== -"@trpc/server@^10.10.0": - version "10.10.0" - resolved "https://registry.yarnpkg.com/@trpc/server/-/server-10.10.0.tgz#0b494335140d4fd5e1452f7b57dcc9b8886720a4" - integrity sha512-tCTqcqBT+3nebYFTHtwM877qo5xQPtVlptxKdUzMVWleWT4lFTL4oddk45qVURToci2iMbVJjd4jQU9y9/XwlQ== +"@trpc/server@^10.13.0": + version "10.13.0" + resolved "https://registry.yarnpkg.com/@trpc/server/-/server-10.13.0.tgz#0945b5210a18934c2284c8679cb6692ecb5b054c" + integrity sha512-d/bu6utCC4ALxhTJkolEPAHMOSuCAu3mG79TZswa6wD2ob0/Z3AIvBF/meeSTqDxe4tvXY78lQqOkQI81dgi/g== "@tryvital/vital-node@^1.4.6": version "1.4.6"