diff --git a/packages/features/auth/lib/getServerSession.ts b/packages/features/auth/lib/getServerSession.ts index 6b8308c4e6..6a43c8fe3a 100644 --- a/packages/features/auth/lib/getServerSession.ts +++ b/packages/features/auth/lib/getServerSession.ts @@ -1,18 +1,85 @@ import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next"; import type { AuthOptions, Session } from "next-auth"; -import { getServerSession as getServerSessionInner } from "next-auth/next"; +import { getToken } from "next-auth/jwt"; -import { AUTH_OPTIONS } from "./next-auth-options"; +import checkLicense from "@calcom/features/ee/common/server/checkLicense"; +import { CAL_URL } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; +/** + * Stores the session in memory using the stringified token as the key. + * + * This is fine for production as each lambda will be recycled before this + * becomes large enough to cause issues. + * + * If we want to get extra spicy we could store this in edge config or similar + * so we can TTL things and benefit from faster retrievals than prisma. + */ +const UNSTABLE_SESSION_CACHE = new Map(); + +/** + * This is a slimmed down version of the `getServerSession` function from + * `next-auth`. + * + * Instead of requiring the entire options object for NextAuth, we create + * a compatible session using information from the incoming token. + * + * The downside to this is that we won't refresh sessions if the users + * token has expired (30 days). This should be fine as we call `/auth/session` + * frequently enough on the client-side to keep the session alive. + */ export async function getServerSession(options: { req: NextApiRequest | GetServerSidePropsContext["req"]; - res: NextApiResponse | GetServerSidePropsContext["res"]; + res?: NextApiResponse | GetServerSidePropsContext["res"]; authOptions?: AuthOptions; }) { - const { req, res, authOptions = AUTH_OPTIONS } = options; + const { req, authOptions: { secret } = {} } = options; - const session = await getServerSessionInner(req, res, authOptions); + const token = await getToken({ + req, + secret, + }); - // that these are equal are ensured in `[...nextauth]`'s callback - return session as Session | null; + if (!token || !token.email || !token.sub) { + return null; + } + + const cachedSession = UNSTABLE_SESSION_CACHE.get(JSON.stringify(token)); + + if (cachedSession) { + return cachedSession; + } + + const user = await prisma.user.findUnique({ + where: { + email: token.email.toLowerCase(), + }, + }); + + if (!user) { + return null; + } + + const hasValidLicense = await checkLicense(prisma); + + const session: Session = { + hasValidLicense, + expires: new Date(typeof token.exp === "number" ? token.exp * 1000 : Date.now()).toISOString(), + user: { + id: user.id, + name: user.name, + username: user.username, + email: user.email, + emailVerified: user.emailVerified, + email_verified: user.emailVerified !== null, + role: user.role, + image: `${CAL_URL}/${user.username}/avatar.png`, + impersonatedByUID: token.impersonatedByUID ?? undefined, + belongsToActiveTeam: token.belongsToActiveTeam, + }, + }; + + UNSTABLE_SESSION_CACHE.set(JSON.stringify(token), session); + + return session; } diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index c0209c1f94..bf366b84e0 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -42,13 +42,16 @@ type UserTeams = { })[]; }; -const checkIfUserBelongsToActiveTeam = (user: T): boolean => - user.teams.filter((m: { team: { metadata: unknown } }) => { - if (!IS_TEAM_BILLING_ENABLED) return true; +export const checkIfUserBelongsToActiveTeam = (user: T) => + user.teams.some((m: { team: { metadata: unknown } }) => { + if (!IS_TEAM_BILLING_ENABLED) { + return true; + } + const metadata = teamMetadataSchema.safeParse(m.team.metadata); - if (metadata.success && metadata.data?.subscriptionId) return true; - return false; - }).length > 0; + + return metadata.success && metadata.data?.subscriptionId; + }); const providers: Provider[] = [ CredentialsProvider({ diff --git a/packages/types/next-auth.d.ts b/packages/types/next-auth.d.ts index ef7beb8e0a..2eb669c7af 100644 --- a/packages/types/next-auth.d.ts +++ b/packages/types/next-auth.d.ts @@ -1,4 +1,4 @@ -import type { User as PrismaUser } from "@prisma/client"; +import type { User as PrismaUser, UserPermissionRole } from "@prisma/client"; import type { DefaultUser } from "next-auth"; declare module "next-auth" { @@ -9,6 +9,7 @@ declare module "next-auth" { hasValidLicense: boolean; user: User; } + interface User extends Omit { id: PrismaUser["id"]; emailVerified?: PrismaUser["emailVerified"]; @@ -19,3 +20,15 @@ declare module "next-auth" { role?: PrismaUser["role"] | "INACTIVE_ADMIN"; } } + +declare module "next-auth/jwt" { + interface JWT { + id?: string | number; + name?: string | null; + username?: string | null; + email?: string | null; + role?: UserPermissionRole | "INACTIVE_ADMIN" | null; + impersonatedByUID?: number | null; + belongsToActiveTeam?: boolean; + } +}