refactor: Remove all TrpcSessionUser usages in @calcom/features (#27853)

* sessionUser in features

* update sessionMiddleware

* update

* format changes

* update import paths

* fix

* fix ts errors

* refactor

* fix

* fix

* fix

* fix

* rename

* rename

* rename

* use error with code objs
This commit is contained in:
Benny Joo authored and GitHub committed 2026-02-24 09:29:54 +00:00
1 parent 48cbb94af2
commit 9855176948
15 files changed
+172 -182

No files matched your search

@@ -0,0 +1,140 @@
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import type { GetServerSidePropsContext, NextApiRequest } from "next";
import type { Session } from "next-auth";
type Maybe<T> = T | null | undefined;
export type SessionContext = {
req?: NextApiRequest | GetServerSidePropsContext["req"];
locale?: string;
session?: Session | null;
};
async function getUserFromSession(ctx: SessionContext, session: Maybe<Session>) {
if (!session) {
return null;
}
if (!session.user?.id) {
return null;
}
const userRepo = new UserRepository(prisma);
const userFromDb = await userRepo.findUnlockedUserForSession({ userId: session.user.id });
// some hacks to make sure `username` and `email` are never inferred as `null`
if (!userFromDb) {
return null;
}
const upId = session.upId;
const user = await userRepo.enrichUserWithTheProfile({
user: userFromDb,
upId,
});
logger.debug(
`getUserFromSession: enriched user with profile - ${ctx.req?.url}`,
safeStringify({ user, userFromDb, upId })
);
const { email, username, id, uuid } = user;
if (!email || !id) {
return null; // should we return null here?
}
const userMetaData = userMetadata.parse(user.metadata || {});
const orgMetadata = teamMetadataSchema.parse(user.profile?.organization?.metadata || {});
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
const locale = user?.locale ?? ctx.locale ?? "en";
const { members = [], ..._organization } = user.profile?.organization || {};
const isOrgAdmin = members.some((member: { role: string }) => ["OWNER", "ADMIN"].includes(member.role));
if (isOrgAdmin) {
logger.debug("User is an org admin", safeStringify({ userId: user.id }));
} else {
logger.debug("User is not an org admin", safeStringify({ userId: user.id }));
}
const organization = {
..._organization,
id: user.profile?.organization?.id ?? null,
isOrgAdmin,
metadata: orgMetadata,
requestedSlug: orgMetadata?.requestedSlug ?? null,
};
return {
...user,
avatar: `${WEBAPP_URL}/${user.username}/avatar.png${organization.id ? `?orgId=${organization.id}` : ""}`,
// TODO: OrgNewSchema - later - We could consolidate the props in user.profile?.organization as organization is a profile thing now.
organization,
organizationId: organization.id,
id,
uuid,
email,
username,
locale,
defaultBookerLayouts: userMetaData?.defaultBookerLayouts || null,
requiresBookerEmailVerification: user.requiresBookerEmailVerification,
};
}
export type UserFromSession = Awaited<ReturnType<typeof getUserFromSession>>;
export const getSession = async (ctx: SessionContext) => {
const { req } = ctx;
const { getServerSession } = await import("@calcom/features/auth/lib/getServerSession");
return req ? await getServerSession({ req }) : null;
};
export const getUserSession = async (ctx: SessionContext) => {
/**
* It is possible that the session and user have already been added to the context by a previous middleware
* or when creating the context
*/
const session = ctx.session || (await getSession(ctx));
const user = session ? await getUserFromSession(ctx, session) : null;
let foundProfile = null;
// Check authorization for profile
if (session?.profileId && user?.id) {
foundProfile = await ProfileRepository.findByUserIdAndProfileId({
userId: user.id,
profileId: session.profileId,
});
if (!foundProfile) {
logger.error(
"Profile not found or not authorized",
safeStringify({ profileId: session.profileId, userId: user?.id })
);
// TODO: Test that logout should happen automatically
throw new ErrorWithCode(ErrorCode.Unauthorized, "Profile not found or not authorized");
}
}
let sessionWithUpId = null;
if (session) {
let upId = session.upId;
if (!upId) {
upId = foundProfile?.upId ?? `usr-${user?.id}`;
}
if (!upId) {
throw new ErrorWithCode(ErrorCode.InternalServerError, "No upId found for session");
}
sessionWithUpId = {
...session,
upId,
};
}
return { user, session: sessionWithUpId };
};
@@ -1,11 +1,8 @@
import prismock from "@calcom/testing/lib/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { describe, expect, it } from "vitest";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import { FilterSegmentRepository } from "../../repositories/filterSegment";
import type { TCreateFilterSegmentInputSchema } from "../../repositories/filterSegment.type";
@@ -16,7 +13,7 @@ describe("FilterSegmentRepository.create()", () => {
const mockUser = {
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
} as NonNullable<UserFromSession>;
const baseInput = {
tableIdentifier: "bookings",
@@ -1,12 +1,9 @@
import prismock from "@calcom/testing/lib/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { describe, expect, it } from "vitest";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import { FilterSegmentRepository } from "../../repositories/filterSegment";
import { type TDeleteFilterSegmentInputSchema } from "../../repositories/filterSegment.type";
import type { TDeleteFilterSegmentInputSchema } from "../../repositories/filterSegment.type";
const repository = new FilterSegmentRepository();
@@ -15,7 +12,7 @@ describe("FilterSegmentRepository.delete()", () => {
const mockUser = {
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
} as NonNullable<UserFromSession>;
it("should delete a user-scoped filter segment", async () => {
// Create a user-scoped segment first
@@ -1,12 +1,9 @@
import prismock from "@calcom/testing/lib/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { describe, expect, it } from "vitest";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import { FilterSegmentRepository } from "../../repositories/filterSegment";
import { type TListFilterSegmentsInputSchema } from "../../repositories/filterSegment.type";
import type { TListFilterSegmentsInputSchema } from "../../repositories/filterSegment.type";
const repository = new FilterSegmentRepository();
@@ -15,7 +12,7 @@ describe("FilterSegmentRepository.get()", () => {
const mockUser = {
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
} as NonNullable<UserFromSession>;
it("should return user-scoped filter segments", async () => {
// Create user-scoped segments
@@ -1,13 +1,10 @@
import prismock from "@calcom/testing/lib/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { describe, expect, it } from "vitest";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import { FilterSegmentRepository } from "../../repositories/filterSegment";
import { type TUpdateFilterSegmentInputSchema } from "../../repositories/filterSegment.type";
import type { TUpdateFilterSegmentInputSchema } from "../../repositories/filterSegment.type";
const repository = new FilterSegmentRepository();
@@ -16,7 +13,7 @@ describe("FilterSegmentRepository.update()", () => {
const mockUser = {
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
} as NonNullable<UserFromSession>;
const baseInput = {
tableIdentifier: "bookings",
@@ -1,11 +1,10 @@
import { canAccessOrganization } from "@calcom/features/ee/sso/lib/saml";
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
const userCanCreateTeamGroupMapping = async (
user: NonNullable<TrpcSessionUser>,
user: { id: number; email: string },
organizationId: number | null,
teamId?: number
) => {
@@ -1,8 +1,6 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import { OrganizationPaymentService } from "./OrganizationPaymentService";
import type { IOrganizationPermissionService } from "./OrganizationPermissionService";
@@ -60,7 +58,7 @@ vi.mock("@calcom/features/ee/billing/di/containers/Billing", () => ({
describe("OrganizationPaymentService", () => {
let service: OrganizationPaymentService;
let mockPermissionService: IOrganizationPermissionService;
const mockUser: TrpcSessionUser = {
const mockUser: UserFromSession = {
id: 1,
email: "test@example.com",
role: "USER",
@@ -1,8 +1,6 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import { OrganizationPermissionService } from "./OrganizationPermissionService";
vi.mock("@calcom/prisma", () => ({
@@ -21,7 +19,7 @@ vi.mock("@calcom/prisma", () => ({
describe("OrganizationPermissionService", () => {
let service: OrganizationPermissionService;
const mockUser: TrpcSessionUser = {
const mockUser: UserFromSession = {
id: 1,
email: "test@example.com",
role: "USER",
@@ -4,8 +4,8 @@ import { hasEditPermissionForUserID } from "@calcom/lib/hasEditPermissionForUser
import { HttpError } from "@calcom/lib/http-error";
import { transformScheduleToAvailabilityForAtom } from "@calcom/lib/schedules/transformers/for-atom";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { z } from "zod";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import { ScheduleRepository } from "../repositories/ScheduleRepository";
export const ZUpdateInputSchema = z.object({
@@ -37,7 +37,7 @@ export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
interface IUpdateScheduleOptions {
input: TUpdateInputSchema;
user: Pick<NonNullable<TrpcSessionUser>, "id" | "defaultScheduleId" | "timeZone">;
user: Pick<NonNullable<UserFromSession>, "id" | "defaultScheduleId" | "timeZone">;
}
export type UpdateScheduleResponse = Awaited<ReturnType<ScheduleService["update"]>>;
@@ -1,142 +1,9 @@
import { setUser as SentrySetUser } from "@sentry/nextjs";
import type { Session } from "next-auth";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getUserSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { setUser as SentrySetUser } from "@sentry/nextjs";
import { TRPCError } from "@trpc/server";
import type { TRPCContextInner } from "../createContext";
import { middleware } from "../trpc";
type Maybe<T> = T | null | undefined;
export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<Session>) {
if (!session) {
return null;
}
if (!session.user?.id) {
return null;
}
const userRepo = new UserRepository(prisma);
const userFromDb = await userRepo.findUnlockedUserForSession({ userId: session.user.id });
// some hacks to make sure `username` and `email` are never inferred as `null`
if (!userFromDb) {
return null;
}
const upId = session.upId;
const user = await userRepo.enrichUserWithTheProfile({
user: userFromDb,
upId,
});
logger.debug(
`getUserFromSession: enriched user with profile - ${ctx.req?.url}`,
safeStringify({ user, userFromDb, upId })
);
const { email, username, id, uuid } = user;
if (!email || !id) {
return null; // should we return null here?
}
const userMetaData = userMetadata.parse(user.metadata || {});
const orgMetadata = teamMetadataSchema.parse(user.profile?.organization?.metadata || {});
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
const locale = user?.locale ?? ctx.locale;
const { members = [], ..._organization } = user.profile?.organization || {};
const isOrgAdmin = members.some((member: { role: string }) => ["OWNER", "ADMIN"].includes(member.role));
if (isOrgAdmin) {
logger.debug("User is an org admin", safeStringify({ userId: user.id }));
} else {
logger.debug("User is not an org admin", safeStringify({ userId: user.id }));
}
const organization = {
..._organization,
id: user.profile?.organization?.id ?? null,
isOrgAdmin,
metadata: orgMetadata,
requestedSlug: orgMetadata?.requestedSlug ?? null,
};
return {
...user,
avatar: `${WEBAPP_URL}/${user.username}/avatar.png${organization.id ? `?orgId=${organization.id}` : ""}`,
// TODO: OrgNewSchema - later - We could consolidate the props in user.profile?.organization as organization is a profile thing now.
organization,
organizationId: organization.id,
id,
uuid,
email,
username,
locale,
defaultBookerLayouts: userMetaData?.defaultBookerLayouts || null,
requiresBookerEmailVerification: user.requiresBookerEmailVerification,
};
}
export type UserFromSession = Awaited<ReturnType<typeof getUserFromSession>>;
export const getSession = async (ctx: TRPCContextInner) => {
const { req } = ctx;
const { getServerSession } = await import("@calcom/features/auth/lib/getServerSession");
return req ? await getServerSession({ req }) : null;
};
export const getUserSession = async (ctx: TRPCContextInner) => {
/**
* It is possible that the session and user have already been added to the context by a previous middleware
* or when creating the context
*/
const session = ctx.session || (await getSession(ctx));
const user = session ? await getUserFromSession(ctx, session) : null;
let foundProfile = null;
// Check authorization for profile
if (session?.profileId && user?.id) {
foundProfile = await ProfileRepository.findByUserIdAndProfileId({
userId: user.id,
profileId: session.profileId,
});
if (!foundProfile) {
logger.error(
"Profile not found or not authorized",
safeStringify({ profileId: session.profileId, userId: user?.id })
);
// TODO: Test that logout should happen automatically
throw new TRPCError({ code: "UNAUTHORIZED", message: "Profile not found or not authorized" });
}
}
let sessionWithUpId = null;
if (session) {
let upId = session.upId;
if (!upId) {
upId = foundProfile?.upId ?? `usr-${user?.id}`;
}
if (!upId) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "No upId found for session" });
}
sessionWithUpId = {
...session,
upId,
};
}
return { user, session: sessionWithUpId };
};
export const isAuthed = middleware(async ({ ctx, next }) => {
const middlewareStart = performance.now();
@@ -1,4 +1,4 @@
import { getSession } from "../../../middlewares/sessionMiddleware";
import { getSession } from "@calcom/features/auth/lib/userFromSessionUtils";
import publicProcedure from "../../../procedures/publicProcedure";
import { ZEventInputSchema } from "../event.schema";
@@ -13,7 +13,7 @@ type Options = {
export const createHandler = async ({ ctx, input }: Options) => {
const { organizationId } = await userCanCreateTeamGroupMapping(
ctx.user,
{ id: ctx.user.id, email: ctx.user.email },
ctx.user.organizationId,
input.teamId
);
@@ -13,7 +13,7 @@ type Options = {
// Delete directory sync connection for a team
export const deleteHandler = async ({ ctx, input }: Options) => {
await userCanCreateTeamGroupMapping(ctx.user, ctx.user.organizationId, input.teamId);
await userCanCreateTeamGroupMapping({ id: ctx.user.id, email: ctx.user.email }, ctx.user.organizationId, input.teamId);
await prisma.dSyncTeamGroupMapping.delete({
where: {
@@ -11,7 +11,7 @@ type Options = {
};
export const getHandler = async ({ ctx }: Options) => {
const { organizationId } = await userCanCreateTeamGroupMapping(ctx.user, ctx.user.organizationId);
const { organizationId } = await userCanCreateTeamGroupMapping({ id: ctx.user.id, email: ctx.user.email }, ctx.user.organizationId);
// Get org teams
const teamsQuery = await prisma.team.findMany({
+1 -1
View File
@@ -1,3 +1,3 @@
import type { UserFromSession } from "./middlewares/sessionMiddleware";
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
export type TrpcSessionUser = UserFromSession;