added more endpoints and validations for publish-pay teams (#209)
## What does the PR do? - Team billing via API Just like the web project, we validate that team has stripe metadata before converting requestedSlug to slug. Co-authored-by: zomars <zomars@me.com>pull/9078/head
parent
8e25b9244c
commit
c016a4343d
|
@ -2,7 +2,10 @@ import { z } from "zod";
|
|||
|
||||
import { _TeamModel as Team } from "@calcom/prisma/zod";
|
||||
|
||||
export const schemaTeamBaseBodyParams = Team.omit({ id: true }).partial({ hideBranding: true });
|
||||
export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true }).partial({
|
||||
hideBranding: true,
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { HttpError } from "@/../../packages/lib/http-error";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "@lib/validations/attendee";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { HttpError } from "@/../../packages/lib/http-error";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "@lib/validations/event-type";
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
|
@ -13,4 +17,18 @@ async function authMiddleware(req: NextApiRequest) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function checkPermissions(
|
||||
req: NextApiRequest,
|
||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||
) {
|
||||
const { userId, prisma, isAdmin } = req;
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||
const team = await prisma.team.findFirst(args);
|
||||
if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: ${role.toString()} required` });
|
||||
return team;
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId";
|
||||
|
||||
import { checkPermissions } from "./_auth-middleware";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /users/{teamId}:
|
||||
|
@ -36,15 +37,4 @@ export async function deleteHandler(req: NextApiRequest) {
|
|||
return { message: `Team with id: ${teamId} deleted successfully` };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, prisma, isAdmin } = req;
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
if (isAdmin) return;
|
||||
/** Only OWNERS can delete teams */
|
||||
const _team = await prisma.team.findFirst({
|
||||
where: { id: teamId, members: { some: { userId, role: "OWNER" } } },
|
||||
});
|
||||
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER required" });
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { purchaseTeamSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId";
|
||||
import { schemaTeamReadPublic, schemaTeamUpdateBodyParams } from "@lib/validations/team";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { Prisma } from ".prisma/client";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /teams/{teamId}:
|
||||
|
@ -35,9 +41,32 @@ export async function patchHandler(req: NextApiRequest) {
|
|||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
/** Only OWNERS and ADMINS can edit teams */
|
||||
const _team = await prisma.team.findFirst({
|
||||
include: { members: true },
|
||||
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
|
||||
let paymentUrl;
|
||||
if (_team.slug === null && data.slug) {
|
||||
data.metadata = {
|
||||
...(_team.metadata as Prisma.JsonObject),
|
||||
requestedSlug: data.slug,
|
||||
};
|
||||
delete data.slug;
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
const checkoutSession = await purchaseTeamSubscription({
|
||||
teamId: _team.id,
|
||||
seats: _team.members.length,
|
||||
userId,
|
||||
});
|
||||
if (!checkoutSession.url)
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed retrieving a checkout session URL.",
|
||||
});
|
||||
paymentUrl = checkoutSession.url;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Perhaps there is a better fix for this?
|
||||
const cloneData: typeof data & {
|
||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||
|
@ -46,7 +75,14 @@ export async function patchHandler(req: NextApiRequest) {
|
|||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||
};
|
||||
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
||||
return { team: schemaTeamReadPublic.parse(team) };
|
||||
const result = {
|
||||
team: schemaTeamReadPublic.parse(team),
|
||||
paymentUrl,
|
||||
};
|
||||
if (!paymentUrl) {
|
||||
delete result.paymentUrl;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { MembershipRole, UserPermissionRole } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
|
||||
|
||||
import { withMiddleware } from "@lib/helpers/withMiddleware";
|
||||
import { schemaQueryTeamId } from "@lib/validations/shared/queryTeamId";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||
|
||||
import authMiddleware, { checkPermissions } from "./_auth-middleware";
|
||||
|
||||
const patchHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { isAdmin } = req;
|
||||
await checkPermissions(req, { in: [MembershipRole.OWNER, MembershipRole.ADMIN] });
|
||||
|
||||
/** We shape the session as required by tRPC rounter */
|
||||
async function sessionGetter() {
|
||||
return {
|
||||
user: {
|
||||
id: req.userId,
|
||||
username: "" /* Not used in this context */,
|
||||
role: isAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER,
|
||||
},
|
||||
hasValidLicense: true,
|
||||
expires: "" /* Not used in this context */,
|
||||
};
|
||||
}
|
||||
|
||||
/** @see https://trpc.io/docs/server-side-calls */
|
||||
const ctx = await createContext({ req, res }, sessionGetter);
|
||||
const caller = viewerRouter.createCaller(ctx);
|
||||
try {
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
const success_url = req.url?.replace("/publish", "");
|
||||
return await caller.teams.publish({ teamId, success_url });
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||
throw new HttpError({ statusCode, message: cause.message });
|
||||
}
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
PATCH: Promise.resolve({ default: defaultResponder(patchHandler) }),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
|
@ -1,5 +1,8 @@
|
|||
import { MembershipRole } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaMembershipPublic } from "@lib/validations/membership";
|
||||
|
@ -24,6 +27,23 @@ import { schemaTeamBodyParams, schemaTeamReadPublic } from "@lib/validations/tea
|
|||
async function postHandler(req: NextApiRequest) {
|
||||
const { prisma, body, userId } = req;
|
||||
const data = schemaTeamBodyParams.parse(body);
|
||||
|
||||
if (data.slug) {
|
||||
const alreadyExist = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: data.slug,
|
||||
},
|
||||
});
|
||||
if (alreadyExist) throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
// Setting slug in metadata, so it can be published later
|
||||
data.metadata = {
|
||||
requestedSlug: data.slug,
|
||||
};
|
||||
delete data.slug;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Perhaps there is a better fix for this?
|
||||
const cloneData: typeof data & {
|
||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||
|
@ -34,9 +54,10 @@ async function postHandler(req: NextApiRequest) {
|
|||
const team = await prisma.team.create({
|
||||
data: {
|
||||
...cloneData,
|
||||
createdAt: new Date(),
|
||||
members: {
|
||||
// We're also creating the relation membership of team ownership in this call.
|
||||
create: { userId, role: "OWNER", accepted: true },
|
||||
create: { userId, role: MembershipRole.OWNER, accepted: true },
|
||||
},
|
||||
},
|
||||
include: { members: true },
|
||||
|
|
Loading…
Reference in New Issue