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:
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,3 +1,3 @@
|
||||
import type { UserFromSession } from "./middlewares/sessionMiddleware";
|
||||
import type { UserFromSession } from "@calcom/features/auth/lib/userFromSessionUtils";
|
||||
|
||||
export type TrpcSessionUser = UserFromSession;
|
||||
Reference in new issue
Block a user