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 <zomars@me.com>
This commit is contained in:
Julius Marminge 2023-02-28 22:40:19 +01:00 committed by GitHub
parent 2529f71770
commit e4b766b9d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 163 additions and 150 deletions

View File

@ -26,6 +26,7 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
const ssg = createProxySSGHelpers({ const ssg = createProxySSGHelpers({
router: appRouter, router: appRouter,
transformer: superjson, transformer: superjson,
// @ts-expect-error FIXME The signature expects req and res. Which we don't have in an SSG context.
ctx: { ctx: {
prisma, prisma,
session: null, session: null,

View File

@ -18,12 +18,17 @@ module.exports = {
"jsx-a11y/role-supports-aria-props": "off", // @see https://github.com/vercel/next.js/issues/27989#issuecomment-897638654 "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/jsx-curly-brace-presence": ["error", { props: "never", children: "never" }],
"react/self-closing-comp": ["error", { component: true, html: true }], "react/self-closing-comp": ["error", { component: true, html: true }],
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn", "warn",
{ vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" }, {
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
},
], ],
"unused-imports/no-unused-imports": "error",
}, },
overrides: [ overrides: [
{ {
@ -45,16 +50,18 @@ module.exports = {
}, },
overrides: [ overrides: [
{ {
files: ["playwright/**/*.{tsx,ts}"], files: ["**/playwright/**/*.{tsx,ts}"],
rules: { rules: {
"@typescript-eslint/no-unused-vars": "off",
"no-undef": "off", "no-undef": "off",
}, },
}, },
], ],
}, },
{ {
files: ["playwright/**/*.{js,jsx}"], files: ["**/playwright/**/*.{js,jsx}"],
rules: { rules: {
"@typescript-eslint/no-unused-vars": "off",
"no-undef": "off", "no-undef": "off",
}, },
}, },

View File

@ -5,12 +5,16 @@
"authors": "zomars", "authors": "zomars",
"version": "1.0.0", "version": "1.0.0",
"main": "index.ts", "main": "index.ts",
"scripts": {
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix"
},
"dependencies": { "dependencies": {
"@tanstack/react-query": "^4.3.9", "@tanstack/react-query": "^4.3.9",
"@trpc/client": "^10.10.0", "@trpc/client": "^10.13.0",
"@trpc/next": "^10.10.0", "@trpc/next": "^10.13.0",
"@trpc/react-query": "^10.10.0", "@trpc/react-query": "^10.13.0",
"@trpc/server": "^10.10.0", "@trpc/server": "^10.13.0",
"superjson": "1.9.1", "superjson": "1.9.1",
"zod": "^3.20.2" "zod": "^3.20.2"
} }

View File

@ -8,11 +8,10 @@ import { getLocaleFromHeaders } from "@calcom/lib/i18n";
import { defaultAvatarSrc } from "@calcom/lib/profile"; import { defaultAvatarSrc } from "@calcom/lib/profile";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import * as trpc from "@trpc/server"; import type { Maybe } from "@trpc/server";
import { Maybe } from "@trpc/server"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import * as trpcNext from "@trpc/server/adapters/next";
type CreateContextOptions = trpcNext.CreateNextContextOptions | GetServerSidePropsContext; type CreateContextOptions = CreateNextContextOptions | GetServerSidePropsContext;
async function getUserFromSession({ async function getUserFromSession({
session, session,
@ -24,6 +23,7 @@ async function getUserFromSession({
if (!session?.user?.id) { if (!session?.user?.id) {
return null; return null;
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: session.user.id, id: session.user.id,
@ -143,5 +143,3 @@ export const createContext = async ({ req, res }: CreateContextOptions, sessionG
res, res,
}; };
}; };
export type Context = trpc.inferAsyncReturnType<typeof createContextInner>;

View File

@ -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 { z } from "zod";
import { getUserAvailability } from "@calcom/core/getUserAvailability"; import { getUserAvailability } from "@calcom/core/getUserAvailability";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule, getWorkingHours } from "@calcom/lib/availability"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule, getWorkingHours } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns"; 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 { 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"; import { TRPCError } from "@trpc/server";
@ -464,7 +469,7 @@ const setupDefaultSchedule = async (userId: number, scheduleId: number, prisma:
}); });
}; };
const isDefaultSchedule = (scheduleId: number, user: Partial<User>) => { const _isDefaultSchedule = (scheduleId: number, user: Partial<User>) => {
return !user.defaultScheduleId || user.defaultScheduleId === scheduleId; return !user.defaultScheduleId || user.defaultScheduleId === scheduleId;
}; };

View File

@ -117,62 +117,69 @@ const EventTypeDuplicateInput = z.object({
length: z.number(), length: z.number(),
}); });
const eventOwnerProcedure = authedProcedure.use(async ({ ctx, rawInput, next }) => { const eventOwnerProcedure = authedProcedure
// Prevent non-owners to update/delete a team event .input(
const event = await ctx.prisma.eventType.findUnique({ z.object({
where: { id: (rawInput as Record<"id", number>)?.id }, id: z.number(),
include: { users: z.array(z.string()).optional().default([]),
users: true, })
team: { )
select: { .use(async ({ ctx, input, next }) => {
members: { // Prevent non-owners to update/delete a team event
select: { const event = await ctx.prisma.eventType.findUnique({
userId: true, where: { id: input.id },
role: true, 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({ export const eventTypesRouter = router({
// REVIEW: What should we name this procedure? // REVIEW: What should we name this procedure?
getByViewer: authedProcedure.query(async ({ ctx }) => { getByViewer: authedProcedure.query(async ({ ctx }) => {
@ -764,9 +771,6 @@ export const eventTypesRouter = router({
webhooks: _webhooks, webhooks: _webhooks,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
schedule: _schedule, schedule: _schedule,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - not typed correctly as its set on SSR
descriptionAsSafeHTML: _descriptionAsSafeHTML,
...rest ...rest
} = eventType; } = eventType;

View File

@ -4,15 +4,17 @@ import { z } from "zod";
import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours"; import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours";
import type { CurrentSeats } from "@calcom/core/getUserAvailability"; import type { CurrentSeats } from "@calcom/core/getUserAvailability";
import { getUserAvailability } 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 { getDefaultEvent } from "@calcom/lib/defaultEvents";
import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds"; import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
import { performance } from "@calcom/lib/server/perfObserver"; import { performance } from "@calcom/lib/server/perfObserver";
import getTimeSlots from "@calcom/lib/slots"; 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 { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { EventBusyDate } from "@calcom/types/Calendar"; import type { EventBusyDate } from "@calcom/types/Calendar";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";

View File

@ -177,7 +177,7 @@ export const viewerTeamsRouter = router({
// If we save slug, we don't need the requestedSlug anymore // If we save slug, we don't need the requestedSlug anymore
const metadataParse = teamMetadataSchema.safeParse(prevTeam.metadata); const metadataParse = teamMetadataSchema.safeParse(prevTeam.metadata);
if (metadataParse.success) { if (metadataParse.success) {
const { requestedSlug, ...cleanMetadata } = metadataParse.data || {}; const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
data.metadata = { data.metadata = {
...cleanMetadata, ...cleanMetadata,
}; };

View File

@ -1,4 +1,4 @@
import { Prisma } from "@prisma/client"; import type { Prisma } from "@prisma/client";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { z } from "zod"; import { z } from "zod";
@ -19,60 +19,55 @@ const webhookIdAndEventTypeIdSchema = z.object({
eventTypeId: z.number().optional(), eventTypeId: z.number().optional(),
}); });
const webhookProcedure = authedProcedure.use(async ({ ctx, rawInput, next }) => { const webhookProcedure = authedProcedure
// Endpoints that just read the logged in user's data - like 'list' don't necessary have any input .input(webhookIdAndEventTypeIdSchema.optional())
if (!rawInput) { .use(async ({ ctx, input, next }) => {
return next(); // Endpoints that just read the logged in user's data - like 'list' don't necessary have any input
} if (!input) return next();
const webhookIdAndEventTypeId = webhookIdAndEventTypeIdSchema.safeParse(rawInput); const { eventTypeId, id } = input;
if (!webhookIdAndEventTypeId.success) {
throw new TRPCError({ code: "PARSE_ERROR" });
}
const { eventTypeId, id } = webhookIdAndEventTypeId.data;
// A webhook is either linked to Event Type or to a user. // A webhook is either linked to Event Type or to a user.
if (eventTypeId) { if (eventTypeId) {
const team = await ctx.prisma.team.findFirst({ const team = await ctx.prisma.team.findFirst({
where: { where: {
eventTypes: { eventTypes: {
some: { some: {
id: eventTypeId, id: eventTypeId,
},
}, },
}, },
}, include: {
include: { members: true,
members: true, },
}, });
});
// Team should be available and the user should be a member of the team // Team should be available and the user should be a member of the team
if (!team?.members.some((membership) => membership.userId === ctx.user.id)) { if (!team?.members.some((membership) => membership.userId === ctx.user.id)) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", 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) { return next();
const authorizedHook = await ctx.prisma.webhook.findFirst({ });
where: {
id: id,
userId: ctx.user.id,
},
});
if (!authorizedHook) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
}
return next();
});
export const webhookRouter = router({ export const webhookRouter = router({
list: webhookProcedure list: webhookProcedure
.input( .input(
z z
.object({ .object({
eventTypeId: z.number().optional(),
appId: z.string().optional(), appId: z.string().optional(),
}) })
.optional() .optional()

View File

@ -890,7 +890,7 @@ export const workflowsRouter = router({
if (!userWorkflow.user?.teams.length && isSMSAction(s.action)) { if (!userWorkflow.user?.teams.length && isSMSAction(s.action)) {
throw new TRPCError({ code: "UNAUTHORIZED" }); throw new TRPCError({ code: "UNAUTHORIZED" });
} }
const { id: stepId, ...stepToAdd } = s; const { id: _stepId, ...stepToAdd } = s;
return stepToAdd; return stepToAdd;
} }
}); });

View File

@ -1,13 +1,12 @@
import superjson from "superjson"; import superjson from "superjson";
import { ZodError } from "zod";
import rateLimit from "@calcom/lib/rateLimit"; import rateLimit from "@calcom/lib/rateLimit";
import { initTRPC, TRPCError } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import { Context } from "./createContext"; import type { createContext } from "./createContext";
const t = initTRPC.context<Context>().create({ const t = initTRPC.context<typeof createContext>().create({
transformer: superjson, transformer: superjson,
}); });
@ -32,18 +31,15 @@ const isAuthedMiddleware = t.middleware(({ ctx, next }) => {
}); });
}); });
const isAdminMiddleware = t.middleware(({ ctx, next }) => { const isAdminMiddleware = isAuthedMiddleware.unstable_pipe(({ ctx, next }) => {
if (!ctx.user || !ctx.session || ctx.user.role !== "ADMIN") { if (ctx.user.role !== "ADMIN") {
throw new TRPCError({ code: "UNAUTHORIZED" }); throw new TRPCError({ code: "UNAUTHORIZED" });
} }
return next({ return next({
ctx: { ctx: { user: ctx.user },
// infers that `user` and `session` are non-nullable to downstream procedures
session: ctx.session,
user: ctx.user,
},
}); });
}); });
interface IRateLimitOptions { interface IRateLimitOptions {
intervalInMs: number; intervalInMs: number;
limit: number; limit: number;

View File

@ -1,3 +1,4 @@
{ {
"extends": "@calcom/tsconfig/react-library.json" "extends": "@calcom/tsconfig/react-library.json",
"include": ["client", "next", "react", "server"]
} }

View File

@ -1,5 +1,5 @@
import { User as PrismaUser } from "@prisma/client"; import type { User as PrismaUser } from "@prisma/client";
import { DefaultUser } from "next-auth"; import type { DefaultUser } from "next-auth";
declare module "next-auth" { declare module "next-auth" {
/** /**

View File

@ -7910,27 +7910,27 @@
javascript-natural-sort "0.7.1" javascript-natural-sort "0.7.1"
lodash "4.17.21" lodash "4.17.21"
"@trpc/client@^10.10.0": "@trpc/client@^10.13.0":
version "10.10.0" version "10.13.0"
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-10.10.0.tgz#5abdbf399639f2fdfe605682d2cdf3f9dba87cb1" resolved "https://registry.yarnpkg.com/@trpc/client/-/client-10.13.0.tgz#8999a7ba068a684629071b77a07c00a141585eca"
integrity sha512-HRVGkOsR4FIYpyQILP84HLbj6pRnLKgxy4AIelTf9d9TxD60M5bNhbR2Uz3hqNSb9a2ppaRJBLv7twlV9b4qHQ== integrity sha512-r4KuN0os2J194lxg5jn4+o3uNlqunLFYptwTHcVW4Q0XGO0ZoTKLHuxT7c9IeDivkAs6G5oVEPiKhptkag36dQ==
"@trpc/next@^10.10.0": "@trpc/next@^10.13.0":
version "10.10.0" version "10.13.0"
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-10.10.0.tgz#083ff327b68b005d60b19af07008377f830ad73a" resolved "https://registry.yarnpkg.com/@trpc/next/-/next-10.13.0.tgz#64393f0d8dbfae7d87e54260dea532702bd1ecdf"
integrity sha512-7d84L2OoF0RW06drTbNGOOggwMes8JxI3Ln/VOIaYeERzwOFNCtWPmGjWCdq4l1SKbXC6+baS+b9n5cXc+euwA== integrity sha512-Q4rnuuiSUXDYv34f8FNUKhEMQFgLJTTJean78YjhG3Aaci+r4sew4hPmRvDRut8fBpa+EtExq+dv1EUbzlXgJg==
dependencies: dependencies:
react-ssr-prepass "^1.5.0" react-ssr-prepass "^1.5.0"
"@trpc/react-query@^10.10.0": "@trpc/react-query@^10.13.0":
version "10.10.0" version "10.13.0"
resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-10.10.0.tgz#1faf32056aa3ee5ecb2ffd0d15e8abbcd7eff911" resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-10.13.0.tgz#ca5a1da0ea126976d7435775f2ba1846cb7fad59"
integrity sha512-Jc/uii1MPevf95/z/W3ufYGHvrFvrtkjxQ8UuXhJCzOgv/FGPqhmA5PH124nLHEgGLBA7zQxHumofhdXosEhUQ== integrity sha512-y4jbojrDFdEl1KBejBoMWIofcUXDHQA8wf01eKMEDV7Jwc7lhq6R1dxYtKzeF+s5wqfnPWFOGZDmB3flzv07Dw==
"@trpc/server@^10.10.0": "@trpc/server@^10.13.0":
version "10.10.0" version "10.13.0"
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-10.10.0.tgz#0b494335140d4fd5e1452f7b57dcc9b8886720a4" resolved "https://registry.yarnpkg.com/@trpc/server/-/server-10.13.0.tgz#0945b5210a18934c2284c8679cb6692ecb5b054c"
integrity sha512-tCTqcqBT+3nebYFTHtwM877qo5xQPtVlptxKdUzMVWleWT4lFTL4oddk45qVURToci2iMbVJjd4jQU9y9/XwlQ== integrity sha512-d/bu6utCC4ALxhTJkolEPAHMOSuCAu3mG79TZswa6wD2ob0/Z3AIvBF/meeSTqDxe4tvXY78lQqOkQI81dgi/g==
"@tryvital/vital-node@^1.4.6": "@tryvital/vital-node@^1.4.6":
version "1.4.6" version "1.4.6"