Removes the getUser logic from createContext, instead use isAuthed (#7902)

* Removes the getUser logic from createContext, instead use isAuthed

* Fix types :party
This commit is contained in:
Alex van Andel
2023-03-23 19:46:33 +01:00
committed by GitHub
parent fa17139e8f
commit e4893c2218
3 changed files with 121 additions and 136 deletions
-1
View File
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
ctx: {
prisma,
session: null,
user: null,
locale,
i18n: _i18n,
},
+19 -97
View File
@@ -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<Session>;
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<ReturnType<typeof getUserFromSession>>;
user?: Omit<
PrismaUser,
| "locale"
| "twoFactorSecret"
| "emailVerified"
| "password"
| "identityProviderId"
| "invitedTo"
| "allowDynamicBooking"
| "verified"
> & {
locale: NonNullable<PrismaUser["locale"]>;
credentials?: Credential[];
selectedCalendars?: Partial<SelectedCalendar>[];
};
i18n: Awaited<ReturnType<typeof serverSideTranslations>>;
} & Partial<CreateContextOptions>;
@@ -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,
+102 -38
View File
@@ -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<Session> }) {
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<typeof createContextInner>().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);