fix: team metadata restriction for stripe (#23327)

This commit is contained in:
Anik Dhabal Babu
2025-08-26 14:24:11 +00:00
committed by GitHub
parent 6e09489685
commit 2ec68ef1aa
12 changed files with 60 additions and 39 deletions
@@ -12,7 +12,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -50,7 +50,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
if (!team) {
const prevTeam = await prisma.team.findFirstOrThrow({ where: { id } });
metadata = teamMetadataSchema.safeParse(prevTeam.metadata);
metadata = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
if (!metadata.success) {
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
}
@@ -80,7 +80,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
}
if (!metadata) {
metadata = teamMetadataSchema.safeParse(team.metadata);
metadata = teamMetadataStrictSchema.safeParse(team.metadata);
if (!metadata.success) {
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
}
+2 -2
View File
@@ -9,7 +9,7 @@ import type { Team, User } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import type { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema, teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
const log = logger.getSubLogger({ prefix: ["orgMigration"] });
@@ -588,7 +588,7 @@ async function dbRemoveTeamFromOrg({ teamId }: { teamId: number }) {
});
}
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
const teamMetadata = teamMetadataStrictSchema.parse(team?.metadata);
try {
return await prisma.team.update({
where: {
@@ -10,14 +10,14 @@ import { Redirect } from "@calcom/lib/redirect";
import { safeStringify } from "@calcom/lib/safeStringify";
import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import billing from "..";
import { TeamBillingPublishResponseStatus, type TeamBilling, type TeamBillingInput } from "./team-billing";
const log = logger.getSubLogger({ prefix: ["TeamBilling"] });
const teamPaymentMetadataSchema = teamMetadataSchema.unwrap();
const teamPaymentMetadataSchema = teamMetadataStrictSchema.unwrap();
export class InternalTeamBilling implements TeamBilling {
private _team!: Omit<TeamBillingInput, "metadata"> & {
@@ -30,7 +30,7 @@ import {
orgOnboardingInvitedMembersSchema,
orgOnboardingTeamsSchema,
} from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { createTeamsHandler } from "@calcom/trpc/server/routers/viewer/organizations/createTeams.handler";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
@@ -344,7 +344,7 @@ async function backwardCompatibilityForSubscriptionDetails({
return organization;
}
const existingMetadata = teamMetadataSchema.parse(organization.metadata);
const existingMetadata = teamMetadataStrictSchema.parse(organization.metadata);
const updatedOrganization = await OrganizationRepository.updateStripeSubscriptionDetails({
id: organization.id,
stripeSubscriptionId: paymentSubscriptionId,
@@ -6,6 +6,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
import type { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { createAProfileForAnExistingUser } from "../../createAProfileForAnExistingUser";
@@ -448,7 +449,7 @@ export class OrganizationRepository {
id: number;
stripeSubscriptionId: string;
stripeSubscriptionItemId: string;
existingMetadata: z.infer<typeof teamMetadataSchema>;
existingMetadata: z.infer<typeof teamMetadataStrictSchema>;
}) {
return await prisma.team.update({
where: { id, isOrganization: true },
+34 -17
View File
@@ -372,24 +372,41 @@ export enum BillingPeriod {
ANNUALLY = "ANNUALLY",
}
export const teamMetadataSchema = z
.object({
defaultConferencingApp: schemaDefaultConferencingApp.optional(),
requestedSlug: z.string().or(z.null()),
paymentId: z.string(),
subscriptionId: z.string().nullable(),
subscriptionItemId: z.string().nullable(),
orgSeats: z.number().nullable(),
orgPricePerSeat: z.number().nullable(),
migratedToOrgFrom: z
.object({
teamSlug: z.string().or(z.null()).optional(),
lastMigrationTime: z.string().optional(),
reverted: z.boolean().optional(),
lastRevertTime: z.string().optional(),
const baseTeamMetadataSchema = z.object({
defaultConferencingApp: schemaDefaultConferencingApp.optional(),
requestedSlug: z.string().or(z.null()),
paymentId: z.string(),
subscriptionId: z.string().nullable(),
subscriptionItemId: z.string().nullable(),
orgSeats: z.number().nullable(),
orgPricePerSeat: z.number().nullable(),
migratedToOrgFrom: z
.object({
teamSlug: z.string().or(z.null()).optional(),
lastMigrationTime: z.string().optional(),
reverted: z.boolean().optional(),
lastRevertTime: z.string().optional(),
})
.optional(),
billingPeriod: z.nativeEnum(BillingPeriod).optional(),
});
export const teamMetadataSchema = baseTeamMetadataSchema.partial().nullable();
export const teamMetadataStrictSchema = baseTeamMetadataSchema
.extend({
subscriptionId: z
.string()
.refine((val) => val.startsWith("sub_"), {
message: "subscriptionId must start with 'sub_'",
})
.optional(),
billingPeriod: z.nativeEnum(BillingPeriod).optional(),
.nullable(),
subscriptionItemId: z
.string()
.refine((val) => val.startsWith("si_"), {
message: "subscriptionItemId must start with 'si_'",
})
.nullable(),
})
.partial()
.nullable();
@@ -4,7 +4,7 @@ import { renameDomain } from "@calcom/lib/domainManager/organization";
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
import { HttpError } from "@calcom/lib/http-error";
import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "../../../types";
import type { TAdminUpdate } from "./adminUpdate.schema";
@@ -34,7 +34,7 @@ export const adminUpdateHandler = async ({ input }: AdminUpdateOptions) => {
});
}
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), existingOrg.metadata || {});
const { mergeMetadata } = getMetadataHelpers(teamMetadataStrictSchema.unwrap(), existingOrg.metadata || {});
const data: Prisma.TeamUpdateArgs["data"] = restInput;
@@ -9,7 +9,7 @@ import slugify from "@calcom/lib/slugify";
import { prisma } from "@calcom/prisma";
import type { CreationSource } from "@calcom/prisma/enums";
import { MembershipRole, RedirectType } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema, teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -295,7 +295,7 @@ async function tryToCancelSubscription(subscriptionId: string) {
}
function getSubscriptionId(metadata: Prisma.JsonValue) {
const parsedMetadata = teamMetadataSchema.safeParse(metadata);
const parsedMetadata = teamMetadataStrictSchema.safeParse(metadata);
if (parsedMetadata.success) {
const subscriptionId = parsedMetadata.data?.subscriptionId;
if (!subscriptionId) {
@@ -3,7 +3,7 @@ import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/pay
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -31,7 +31,7 @@ export const publishHandler = async ({ ctx }: PublishOptions) => {
if (!prevTeam) throw new TRPCError({ code: "NOT_FOUND", message: "Organization not found." });
const metadata = teamMetadataSchema.safeParse(prevTeam.metadata);
const metadata = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
if (!metadata.success) throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid team metadata" });
// Since this is an ORG we need to make sure ORG members are scyned with the team. Every time a user is added to the TEAM, we need to add them to the ORG
@@ -9,7 +9,7 @@ import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
import type { PrismaClient } from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -165,7 +165,10 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
if (!prevOrganisation) throw new TRPCError({ code: "NOT_FOUND", message: "Organisation not found." });
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata ?? {});
const { mergeMetadata } = getMetadataHelpers(
teamMetadataStrictSchema.unwrap(),
prevOrganisation.metadata ?? {}
);
const data: Prisma.TeamUpdateArgs["data"] = {
logoUrl: input.logoUrl,
@@ -1,7 +1,7 @@
import { z } from "zod";
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
export const ZUpdateInputSchema = z.object({
name: z.string().optional(),
@@ -28,7 +28,7 @@ export const ZUpdateInputSchema = z.object({
timeZone: timeZoneSchema.optional(),
weekStart: z.string().optional(),
timeFormat: z.number().optional(),
metadata: teamMetadataSchema.unwrap().optional(),
metadata: teamMetadataStrictSchema.unwrap().optional(),
lockEventTypeCreation: z.boolean().optional(),
lockEventTypeCreationOptions: z.enum(["DELETE", "HIDE"]).optional(),
adminGetsNoSlotsNotification: z.boolean().optional(),
@@ -8,7 +8,7 @@ import { uploadLogo } from "@calcom/lib/server/avatar";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import { prisma } from "@calcom/prisma";
import { RedirectType, RRTimestampBasis } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -95,7 +95,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
data.slug = input.slug;
// If we save slug, we don't need the requestedSlug anymore
const metadataParse = teamMetadataSchema.safeParse(prevTeam.metadata);
const metadataParse = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
if (metadataParse.success) {
const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
data.metadata = {