Skip to content

Commit 4a12107

Browse files
committed
restrict write operstion only
1 parent 0d8633b commit 4a12107

15 files changed

Lines changed: 71 additions & 111 deletions

File tree

apps/api/v2/src/modules/teams/teams/teams.repository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
22
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
33
import { Injectable, NotFoundException } from "@nestjs/common";
44

5-
import { teamMetadataSchema } from "@calcom/platform-libraries";
5+
import { teamMetadataStrictSchema } from "@calcom/platform-libraries";
66
import { Prisma } from "@calcom/prisma/client";
77

88
@Injectable()
@@ -87,7 +87,7 @@ export class TeamsRepository {
8787

8888
async setDefaultConferencingApp(teamId: number, appSlug?: string, appLink?: string) {
8989
const team = await this.getById(teamId);
90-
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
90+
const teamMetadata = teamMetadataStrictSchema.parse(team?.metadata);
9191

9292
if (!team) {
9393
throw new NotFoundException("user not found");

apps/web/app/api/teams/[team]/upgrade/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
1212
import { WEBAPP_URL } from "@calcom/lib/constants";
1313
import { HttpError } from "@calcom/lib/http-error";
1414
import prisma from "@calcom/prisma";
15-
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
15+
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
1616

1717
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
1818

@@ -50,7 +50,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
5050
if (!team) {
5151
const prevTeam = await prisma.team.findFirstOrThrow({ where: { id } });
5252

53-
metadata = teamMetadataSchema.safeParse(prevTeam.metadata);
53+
metadata = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
5454
if (!metadata.success) {
5555
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
5656
}
@@ -80,7 +80,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
8080
}
8181

8282
if (!metadata) {
83-
metadata = teamMetadataSchema.safeParse(team.metadata);
83+
metadata = teamMetadataStrictSchema.safeParse(team.metadata);
8484
if (!metadata.success) {
8585
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
8686
}

apps/web/playwright/lib/orgMigration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { Team, User } from "@calcom/prisma/client";
99
import { RedirectType } from "@calcom/prisma/client";
1010
import { Prisma } from "@calcom/prisma/client";
1111
import type { MembershipRole } from "@calcom/prisma/enums";
12-
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
12+
import { teamMetadataSchema, teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
1313

1414
const log = logger.getSubLogger({ prefix: ["orgMigration"] });
1515

@@ -588,7 +588,7 @@ async function dbRemoveTeamFromOrg({ teamId }: { teamId: number }) {
588588
});
589589
}
590590

591-
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
591+
const teamMetadata = teamMetadataStrictSchema.parse(team?.metadata);
592592
try {
593593
return await prisma.team.update({
594594
where: {

packages/features/ee/billing/teams/internal-team-billing.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import { Redirect } from "@calcom/lib/redirect";
1010
import { safeStringify } from "@calcom/lib/safeStringify";
1111
import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
1212
import prisma from "@calcom/prisma";
13-
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
13+
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
1414

1515
import billing from "..";
1616
import { TeamBillingPublishResponseStatus, type TeamBilling, type TeamBillingInput } from "./team-billing";
1717

1818
const log = logger.getSubLogger({ prefix: ["TeamBilling"] });
1919

20-
const teamPaymentMetadataSchema = teamMetadataSchema.unwrap();
20+
const teamPaymentMetadataSchema = teamMetadataStrictSchema.unwrap();
2121

2222
export class InternalTeamBilling implements TeamBilling {
2323
private _team!: Omit<TeamBillingInput, "metadata"> & {

packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
orgOnboardingInvitedMembersSchema,
3131
orgOnboardingTeamsSchema,
3232
} from "@calcom/prisma/zod-utils";
33-
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
33+
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
3434
import { createTeamsHandler } from "@calcom/trpc/server/routers/viewer/organizations/createTeams.handler";
3535
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
3636

@@ -344,7 +344,7 @@ async function backwardCompatibilityForSubscriptionDetails({
344344
return organization;
345345
}
346346

347-
const existingMetadata = teamMetadataSchema.parse(organization.metadata);
347+
const existingMetadata = teamMetadataStrictSchema.parse(organization.metadata);
348348
const updatedOrganization = await OrganizationRepository.updateStripeSubscriptionDetails({
349349
id: organization.id,
350350
stripeSubscriptionId: paymentSubscriptionId,

packages/features/ee/teams/lib/payments.test.ts

Lines changed: 18 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,8 @@ describe("updateQuantitySubscriptionFromStripe", () => {
338338
describe("For an organization", () => {
339339
it("should not update subscription when team members are less than metadata.orgSeats", async () => {
340340
const FAKE_PAYMENT_ID = "FAKE_PAYMENT_ID";
341-
const FAKE_SUBITEM_ID = "si_FAKE_SUBITEM_ID";
342-
const FAKE_SUB_ID = "sub_FAKE_SUB_ID";
341+
const FAKE_SUBITEM_ID = "FAKE_SUBITEM_ID";
342+
const FAKE_SUB_ID = "FAKE_SUB_ID";
343343
const FAKE_SUBSCRIPTION_QTY_IN_STRIPE = 1000;
344344
const consoleInfoSpy = vi.spyOn(console, "info");
345345

@@ -363,7 +363,7 @@ describe("updateQuantitySubscriptionFromStripe", () => {
363363
items: {
364364
data: [
365365
{
366-
id: FAKE_SUBITEM_ID,
366+
id: "FAKE_SUBITEM_ID",
367367
quantity: FAKE_SUBSCRIPTION_QTY_IN_STRIPE,
368368
},
369369
],
@@ -384,8 +384,8 @@ describe("updateQuantitySubscriptionFromStripe", () => {
384384

385385
it("should update subscription when team members are more than metadata.orgSeats", async () => {
386386
const FAKE_PAYMENT_ID = "FAKE_PAYMENT_ID";
387-
const FAKE_SUBITEM_ID = "si_FAKE_SUBITEM_ID";
388-
const FAKE_SUB_ID = "sub_FAKE_SUB_ID";
387+
const FAKE_SUB_ID = "FAKE_SUB_ID";
388+
const FAKE_SUBITEM_ID = "FAKE_SUBITEM_ID";
389389
const FAKE_SUBSCRIPTION_QTY_IN_STRIPE = 1000;
390390
const membersInTeam = 4;
391391
const organization = await createOrgWithMembersAndPaymentData({
@@ -434,8 +434,8 @@ describe("updateQuantitySubscriptionFromStripe", () => {
434434

435435
it("should not update subscription when team members are less than MINIMUM_NUMBER_OF_ORG_SEATS(if metadata.orgSeats is null)", async () => {
436436
const FAKE_PAYMENT_ID = "FAKE_PAYMENT_ID";
437-
const FAKE_SUBITEM_ID = "si_FAKE_SUBITEM_ID";
438-
const FAKE_SUB_ID = "sub_FAKE_SUB_ID";
437+
const FAKE_SUBITEM_ID = "FAKE_SUBITEM_ID";
438+
const FAKE_SUB_ID = "FAKE_SUB_ID";
439439
const FAKE_SUBSCRIPTION_QTY_IN_STRIPE = 1000;
440440
const membersInTeam = 2;
441441
const consoleInfoSpy = vi.spyOn(console, "info");
@@ -453,7 +453,7 @@ describe("updateQuantitySubscriptionFromStripe", () => {
453453
items: {
454454
data: [
455455
{
456-
id: FAKE_SUBITEM_ID,
456+
id: "FAKE_SUBITEM_ID",
457457
quantity: FAKE_SUBSCRIPTION_QTY_IN_STRIPE,
458458
},
459459
],
@@ -480,8 +480,8 @@ describe("updateQuantitySubscriptionFromStripe", () => {
480480

481481
it("should update subscription when team members are more than MINIMUM_NUMBER_OF_ORG_SEATS(if metadata.orgSeats is null)", async () => {
482482
const FAKE_PAYMENT_ID = "FAKE_PAYMENT_ID";
483-
const FAKE_SUBITEM_ID = "si_FAKE_SUBITEM_ID";
484-
const FAKE_SUB_ID = "sub_FAKE_SUB_ID";
483+
const FAKE_SUB_ID = "FAKE_SUB_ID";
484+
const FAKE_SUBITEM_ID = "FAKE_SUBITEM_ID";
485485
const FAKE_SUBSCRIPTION_QTY_IN_STRIPE = 1000;
486486
const membersInTeam = 35;
487487
const organization = await createOrgWithMembersAndPaymentData({
@@ -537,8 +537,8 @@ describe("getTeamWithPaymentMetadata", () => {
537537
isOrganization: true,
538538
name: "TestTeam",
539539
metadata: {
540-
subscriptionId: "sub_FAKE_SUB_ID",
541-
subscriptionItemId: "si_FAKE_SUB_ITEM_ID",
540+
subscriptionId: "FAKE_SUB_ID",
541+
subscriptionItemId: "FAKE_SUB_ITEM_ID",
542542
},
543543
},
544544
});
@@ -552,7 +552,7 @@ describe("getTeamWithPaymentMetadata", () => {
552552
name: "TestTeam",
553553
metadata: {
554554
paymentId: "FAKE_PAY_ID",
555-
subscriptionItemId: "si_FAKE_SUB_ITEM_ID",
555+
subscriptionItemId: "FAKE_SUB_ITEM_ID",
556556
},
557557
},
558558
});
@@ -566,7 +566,7 @@ describe("getTeamWithPaymentMetadata", () => {
566566
name: "TestTeam",
567567
metadata: {
568568
paymentId: "FAKE_PAY_ID",
569-
subscriptionId: "sub_FAKE_SUB_ID",
569+
subscriptionId: "FAKE_SUB_ID",
570570
},
571571
},
572572
});
@@ -580,8 +580,8 @@ describe("getTeamWithPaymentMetadata", () => {
580580
name: "TestTeam",
581581
metadata: {
582582
paymentId: "FAKE_PAY_ID",
583-
subscriptionId: "sub_FAKE_SUB_ID",
584-
subscriptionItemId: "si_FAKE_SUB_ITEM_ID",
583+
subscriptionId: "FAKE_SUB_ID",
584+
subscriptionItemId: "FAKE_SUB_ITEM_ID",
585585
},
586586
},
587587
});
@@ -597,65 +597,14 @@ describe("getTeamWithPaymentMetadata", () => {
597597
metadata: {
598598
orgSeats: 5,
599599
paymentId: "FAKE_PAY_ID",
600-
subscriptionId: "sub_FAKE_SUB_ID",
601-
subscriptionItemId: "si_FAKE_SUB_ITEM_ID",
600+
subscriptionId: "FAKE_SUB_ID",
601+
subscriptionItemId: "FAKE_SUB_ITEM_ID",
602602
},
603603
},
604604
});
605605
const teamWithPaymentData = await getTeamWithPaymentMetadata(team.id);
606606
expect(teamWithPaymentData.metadata.orgSeats).toEqual(5);
607607
});
608-
609-
it("should error if subscriptionId doesn't start with 'sub_'", async () => {
610-
const team = await prismock.team.create({
611-
data: {
612-
isOrganization: true,
613-
name: "TestTeam",
614-
metadata: {
615-
paymentId: "FAKE_PAY_ID",
616-
subscriptionId: "invalid_sub_id",
617-
subscriptionItemId: "si_FAKE_SUB_ITEM_ID",
618-
},
619-
},
620-
});
621-
expect(() => getTeamWithPaymentMetadata(team.id)).rejects.toThrow(
622-
"subscriptionId must start with 'sub_'"
623-
);
624-
});
625-
626-
it("should error if subscriptionItemId doesn't start with 'si_'", async () => {
627-
const team = await prismock.team.create({
628-
data: {
629-
isOrganization: true,
630-
name: "TestTeam",
631-
metadata: {
632-
paymentId: "FAKE_PAY_ID",
633-
subscriptionId: "sub_FAKE_SUB_ID",
634-
subscriptionItemId: "invalid_item_id",
635-
},
636-
},
637-
});
638-
expect(() => getTeamWithPaymentMetadata(team.id)).rejects.toThrow(
639-
"subscriptionItemId must start with 'si_'"
640-
);
641-
});
642-
643-
it("should parse successfully with valid prefixes", async () => {
644-
const team = await prismock.team.create({
645-
data: {
646-
isOrganization: true,
647-
name: "TestTeam",
648-
metadata: {
649-
paymentId: "FAKE_PAY_ID",
650-
subscriptionId: "sub_FAKE_SUB_ID",
651-
subscriptionItemId: "si_FAKE_SUB_ITEM_ID",
652-
},
653-
},
654-
});
655-
const teamWithPaymentData = await getTeamWithPaymentMetadata(team.id);
656-
expect(teamWithPaymentData.metadata.subscriptionId).toEqual("sub_FAKE_SUB_ID");
657-
expect(teamWithPaymentData.metadata.subscriptionItemId).toEqual("si_FAKE_SUB_ITEM_ID");
658-
});
659608
});
660609

661610
async function createOrgWithMembersAndPaymentData({

packages/features/ee/teams/lib/payments.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const log = logger.getSubLogger({ prefix: ["teams/lib/payments"] });
2121
const teamPaymentMetadataSchema = z.object({
2222
// Redefine paymentId, subscriptionId and subscriptionItemId to ensure that they are present and nonNullable
2323
paymentId: z.string(),
24-
subscriptionId: z.string().startsWith("sub_", "subscriptionId must start with 'sub_'"),
25-
subscriptionItemId: z.string().startsWith("si_", "subscriptionItemId must start with 'si_'"),
24+
subscriptionId: z.string(),
25+
subscriptionItemId: z.string(),
2626
orgSeats: teamMetadataSchema.unwrap().shape.orgSeats,
2727
});
2828

packages/lib/server/repository/organization.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
66
import { prisma } from "@calcom/prisma";
77
import { MembershipRole } from "@calcom/prisma/enums";
88
import type { CreationSource } from "@calcom/prisma/enums";
9+
import type { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
910
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
1011

1112
import { createAProfileForAnExistingUser } from "../../createAProfileForAnExistingUser";
@@ -448,7 +449,7 @@ export class OrganizationRepository {
448449
id: number;
449450
stripeSubscriptionId: string;
450451
stripeSubscriptionItemId: string;
451-
existingMetadata: z.infer<typeof teamMetadataSchema>;
452+
existingMetadata: z.infer<typeof teamMetadataStrictSchema>;
452453
}) {
453454
return await prisma.team.update({
454455
where: { id, isOrganization: true },

packages/prisma/zod-utils.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,29 @@ export enum BillingPeriod {
372372
ANNUALLY = "ANNUALLY",
373373
}
374374

375-
export const teamMetadataSchema = z
376-
.object({
377-
defaultConferencingApp: schemaDefaultConferencingApp.optional(),
378-
requestedSlug: z.string().or(z.null()),
379-
paymentId: z.string(),
375+
const baseTeamMetadataSchema = z.object({
376+
defaultConferencingApp: schemaDefaultConferencingApp.optional(),
377+
requestedSlug: z.string().or(z.null()),
378+
paymentId: z.string(),
379+
subscriptionId: z.string().nullable(),
380+
subscriptionItemId: z.string().nullable(),
381+
orgSeats: z.number().nullable(),
382+
orgPricePerSeat: z.number().nullable(),
383+
migratedToOrgFrom: z
384+
.object({
385+
teamSlug: z.string().or(z.null()).optional(),
386+
lastMigrationTime: z.string().optional(),
387+
reverted: z.boolean().optional(),
388+
lastRevertTime: z.string().optional(),
389+
})
390+
.optional(),
391+
billingPeriod: z.nativeEnum(BillingPeriod).optional(),
392+
});
393+
394+
export const teamMetadataSchema = baseTeamMetadataSchema.partial().nullable();
395+
396+
export const teamMetadataStrictSchema = baseTeamMetadataSchema
397+
.extend({
380398
subscriptionId: z
381399
.string()
382400
.refine((val) => val.startsWith("sub_"), {
@@ -389,17 +407,6 @@ export const teamMetadataSchema = z
389407
message: "subscriptionItemId must start with 'si_'",
390408
})
391409
.nullable(),
392-
orgSeats: z.number().nullable(),
393-
orgPricePerSeat: z.number().nullable(),
394-
migratedToOrgFrom: z
395-
.object({
396-
teamSlug: z.string().or(z.null()).optional(),
397-
lastMigrationTime: z.string().optional(),
398-
reverted: z.boolean().optional(),
399-
lastRevertTime: z.string().optional(),
400-
})
401-
.optional(),
402-
billingPeriod: z.nativeEnum(BillingPeriod).optional(),
403410
})
404411
.partial()
405412
.nullable();

packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { renameDomain } from "@calcom/lib/domainManager/organization";
44
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
55
import { HttpError } from "@calcom/lib/http-error";
66
import { prisma } from "@calcom/prisma";
7-
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
7+
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
88

99
import type { TrpcSessionUser } from "../../../types";
1010
import type { TAdminUpdate } from "./adminUpdate.schema";
@@ -34,7 +34,7 @@ export const adminUpdateHandler = async ({ input }: AdminUpdateOptions) => {
3434
});
3535
}
3636

37-
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), existingOrg.metadata || {});
37+
const { mergeMetadata } = getMetadataHelpers(teamMetadataStrictSchema.unwrap(), existingOrg.metadata || {});
3838

3939
const data: Prisma.TeamUpdateArgs["data"] = restInput;
4040

0 commit comments

Comments
 (0)