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({
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,

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
"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",
},
},

View File

@ -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"
}

View File

@ -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<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 { 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<User>) => {
const _isDefaultSchedule = (scheduleId: number, user: Partial<User>) => {
return !user.defaultScheduleId || user.defaultScheduleId === scheduleId;
};

View File

@ -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;

View File

@ -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";

View File

@ -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,
};

View File

@ -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()

View File

@ -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;
}
});

View File

@ -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<Context>().create({
const t = initTRPC.context<typeof createContext>().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;

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 { DefaultUser } from "next-auth";
import type { User as PrismaUser } from "@prisma/client";
import type { DefaultUser } from "next-auth";
declare module "next-auth" {
/**

View File

@ -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"