-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: allow organization owners access to shared billing portal #23451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
94f6f17
16da3ce
3e287c0
f6265de
b743d77
e41ec99
205c22b
0783b39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,29 +2,75 @@ import type { NextApiRequest, NextApiResponse } from "next"; | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { WEBAPP_URL } from "@calcom/lib/constants"; | ||||||||||||||||||||||||||||||
| import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; | ||||||||||||||||||||||||||||||
| import { TeamRepository } from "@calcom/lib/server/repository/team"; | ||||||||||||||||||||||||||||||
| import prisma from "@calcom/prisma"; | ||||||||||||||||||||||||||||||
| import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { getStripeCustomerIdFromUserId } from "../lib/customer"; | ||||||||||||||||||||||||||||||
| import stripe from "../lib/server"; | ||||||||||||||||||||||||||||||
| import { getSubscriptionFromId } from "../lib/subscriptions"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||||||||||||||||||||||||||||||
| if (req.method !== "POST" && req.method !== "GET") | ||||||||||||||||||||||||||||||
| return res.status(405).json({ message: "Method not allowed" }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // if (!referer) return res.status(400).json({ message: "Missing referrer" }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!req.session?.user?.id) return res.status(401).json({ message: "Not authenticated" }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // If accessing a user's portal | ||||||||||||||||||||||||||||||
| const customerId = await getStripeCustomerIdFromUserId(req.session.user.id); | ||||||||||||||||||||||||||||||
| if (!customerId) return res.status(400).json({ message: "CustomerId not found in stripe" }); | ||||||||||||||||||||||||||||||
| const userId = req.session.user.id; | ||||||||||||||||||||||||||||||
| const teamId = req.query.teamId ? parseInt(req.query.teamId as string) : null; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!teamId) { | ||||||||||||||||||||||||||||||
| return res.status(400).json({ message: "Team ID is required" }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const teamRepository = new TeamRepository(prisma); | ||||||||||||||||||||||||||||||
| const team = await teamRepository.getTeamByIdIfUserIsAdmin({ | ||||||||||||||||||||||||||||||
| teamId, | ||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Only query TeamRepository when team scope is present. Avoid hitting the DB when teamId isn’t provided; set team conditionally. Apply this diff: - const teamRepository = new TeamRepository(prisma);
- const team = await teamRepository.getTeamByIdIfUserIsAdmin({
- teamId,
- userId,
- });
+ let team: any = null;
+ if (hasTeamScope) {
+ const teamRepository = new TeamRepository(prisma);
+ team = await teamRepository.getTeamByIdIfUserIsAdmin({
+ teamId: teamId!,
+ userId,
+ });
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| let return_url = `${WEBAPP_URL}/settings/billing`; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (typeof req.query.returnTo === "string") { | ||||||||||||||||||||||||||||||
| const safeRedirectUrl = getSafeRedirectUrl(req.query.returnTo); | ||||||||||||||||||||||||||||||
| if (safeRedirectUrl) return_url = safeRedirectUrl; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!team) { | ||||||||||||||||||||||||||||||
| const customerId = await getStripeCustomerIdFromUserId(userId); | ||||||||||||||||||||||||||||||
| if (!customerId) return res.status(404).json({ message: "CustomerId not found" }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const portalSession = await stripe.billingPortal.sessions.create({ | ||||||||||||||||||||||||||||||
| customer: customerId, | ||||||||||||||||||||||||||||||
| return_url, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return res.status(200).json({ url: portalSession.url }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const teamMetadataParsed = teamMetadataSchema.safeParse(team.metadata); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!teamMetadataParsed.success) { | ||||||||||||||||||||||||||||||
| return res.status(400).json({ message: "Invalid team metadata" }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!teamMetadataParsed.data?.subscriptionId) { | ||||||||||||||||||||||||||||||
| return res.status(400).json({ message: "subscriptionId not found for team" }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const subscription = await getSubscriptionFromId(teamMetadataParsed.data.subscriptionId); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!subscription) { | ||||||||||||||||||||||||||||||
| return res.status(400).json({ message: "Subscription not found" }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!subscription.customer) { | ||||||||||||||||||||||||||||||
| return res.status(400).json({ message: "Subscription customer not found" }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const customerId = subscription.customer as string; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
joeauyeung marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
| if (!customerId) return res.status(400).json({ message: "CustomerId not found in stripe" }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const stripeSession = await stripe.billingPortal.sessions.create({ | ||||||||||||||||||||||||||||||
| customer: customerId, | ||||||||||||||||||||||||||||||
| return_url, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this endpoint we were only showing the portal if the requesting user id matched the specific customer id tied to the subscription on Stripe. Now we determine if the requesting user is an owner/admin of the team then redirect them to the billing portal.