Skip to content

Commit 6ebbcee

Browse files
committed
fix(webapp): restore manage:billing enforcement on billing + billing-alerts routes
These two routes reverted to raw loaders/actions when main's changes were taken during a merge conflict. Re-apply the dashboardLoader/dashboardAction migration with a manage:billing authorization block on top of main's current code (which added the showSelfServe branching), keeping the isManagedCloud guard and membership queries.
1 parent 65fbca5 commit 6ebbcee

2 files changed

Lines changed: 172 additions & 133 deletions

File tree

  • apps/webapp/app/routes
    • _app.orgs.$organizationSlug.settings.billing-alerts
    • _app.orgs.$organizationSlug.settings.billing

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-alerts/route.tsx

Lines changed: 106 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { conform, list, requestIntent, useFieldList, useForm } from "@conform-to/react";
22
import { parse } from "@conform-to/zod";
33
import { Form, useActionData, type MetaFunction } from "@remix-run/react";
4-
import { json, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/server-runtime";
4+
import { json } from "@remix-run/server-runtime";
55
import { tryCatch } from "@trigger.dev/core";
66
import { Fragment, useEffect, useRef, useState } from "react";
77
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
@@ -25,11 +25,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
2525
import { Paragraph } from "~/components/primitives/Paragraph";
2626
import { TextLink } from "~/components/primitives/TextLink";
2727
import { InfoIconTooltip } from "~/components/primitives/Tooltip";
28-
import { prisma } from "~/db.server";
28+
import { $replica, prisma } from "~/db.server";
2929
import { featuresForRequest } from "~/features.server";
3030
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
3131
import { getBillingAlerts, getCurrentPlan, setBillingAlert } from "~/services/platform.v3.server";
32-
import { requireUserId } from "~/services/session.server";
32+
import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
3333
import { formatCurrency, formatNumber } from "~/utils/numberFormatter";
3434
import {
3535
docsPath,
@@ -48,44 +48,59 @@ export const meta: MetaFunction = () => {
4848
];
4949
};
5050

51-
export async function loader({ params, request }: LoaderFunctionArgs) {
52-
const userId = await requireUserId(request);
53-
const { organizationSlug } = OrganizationParamsSchema.parse(params);
51+
async function resolveOrgIdFromSlug(slug: string): Promise<string | null> {
52+
const org = await $replica.organization.findFirst({ where: { slug }, select: { id: true } });
53+
return org?.id ?? null;
54+
}
5455

55-
const { isManagedCloud } = featuresForRequest(request);
56-
if (!isManagedCloud) {
57-
return redirect(organizationPath({ slug: organizationSlug }));
58-
}
56+
export const loader = dashboardLoader(
57+
{
58+
params: OrganizationParamsSchema,
59+
context: async (params) => {
60+
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
61+
return organizationId ? { organizationId } : {};
62+
},
63+
authorization: { action: "manage", resource: { type: "billing" } },
64+
},
65+
async ({ params, request, user }) => {
66+
const userId = user.id;
67+
const { organizationSlug } = params;
5968

60-
const organization = await prisma.organization.findFirst({
61-
where: { slug: organizationSlug, members: { some: { userId } } },
62-
});
69+
const { isManagedCloud } = featuresForRequest(request);
70+
if (!isManagedCloud) {
71+
return redirect(organizationPath({ slug: organizationSlug }));
72+
}
6373

64-
if (!organization) {
65-
throw new Response(null, { status: 404, statusText: "Organization not found" });
66-
}
74+
const organization = await prisma.organization.findFirst({
75+
where: { slug: organizationSlug, members: { some: { userId } } },
76+
});
6777

68-
const currentPlan = await getCurrentPlan(organization.id);
69-
if (currentPlan?.v3Subscription?.showSelfServe === false) {
70-
return redirect(v3BillingPath({ slug: organizationSlug }));
71-
}
78+
if (!organization) {
79+
throw new Response(null, { status: 404, statusText: "Organization not found" });
80+
}
7281

73-
const [error, alerts] = await tryCatch(getBillingAlerts(organization.id));
74-
if (error) {
75-
throw new Response(null, { status: 404, statusText: `Billing alerts error: ${error}` });
76-
}
82+
const currentPlan = await getCurrentPlan(organization.id);
83+
if (currentPlan?.v3Subscription?.showSelfServe === false) {
84+
return redirect(v3BillingPath({ slug: organizationSlug }));
85+
}
7786

78-
if (!alerts) {
79-
throw new Response(null, { status: 404, statusText: "Billing alerts not found" });
80-
}
87+
const [error, alerts] = await tryCatch(getBillingAlerts(organization.id));
88+
if (error) {
89+
throw new Response(null, { status: 404, statusText: `Billing alerts error: ${error}` });
90+
}
8191

82-
return typedjson({
83-
alerts: {
84-
...alerts,
85-
amount: alerts.amount / 100,
86-
},
87-
});
88-
}
92+
if (!alerts) {
93+
throw new Response(null, { status: 404, statusText: "Billing alerts not found" });
94+
}
95+
96+
return typedjson({
97+
alerts: {
98+
...alerts,
99+
amount: alerts.amount / 100,
100+
},
101+
});
102+
}
103+
);
89104

90105
const schema = z.object({
91106
amount: z
@@ -110,66 +125,76 @@ const schema = z.object({
110125
}, z.coerce.number().array().nonempty("At least one alert level is required")),
111126
});
112127

113-
export const action: ActionFunction = async ({ request, params }) => {
114-
const userId = await requireUserId(request);
115-
const { organizationSlug } = OrganizationParamsSchema.parse(params);
128+
export const action = dashboardAction(
129+
{
130+
params: OrganizationParamsSchema,
131+
context: async (params) => {
132+
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
133+
return organizationId ? { organizationId } : {};
134+
},
135+
authorization: { action: "manage", resource: { type: "billing" } },
136+
},
137+
async ({ request, params, user }) => {
138+
const userId = user.id;
139+
const { organizationSlug } = params;
116140

117-
const organization = await prisma.organization.findFirst({
118-
where: { slug: organizationSlug, members: { some: { userId } } },
119-
});
141+
const organization = await prisma.organization.findFirst({
142+
where: { slug: organizationSlug, members: { some: { userId } } },
143+
});
120144

121-
if (!organization) {
122-
return redirectWithErrorMessage(
123-
v3BillingPath({ slug: organizationSlug }),
124-
request,
125-
"You are not authorized to update billing alerts"
126-
);
127-
}
145+
if (!organization) {
146+
return redirectWithErrorMessage(
147+
v3BillingPath({ slug: organizationSlug }),
148+
request,
149+
"You are not authorized to update billing alerts"
150+
);
151+
}
128152

129-
const currentPlan = await getCurrentPlan(organization.id);
130-
if (currentPlan?.v3Subscription?.showSelfServe === false) {
131-
return redirect(v3BillingPath({ slug: organizationSlug }));
132-
}
153+
const currentPlan = await getCurrentPlan(organization.id);
154+
if (currentPlan?.v3Subscription?.showSelfServe === false) {
155+
return redirect(v3BillingPath({ slug: organizationSlug }));
156+
}
133157

134-
const formData = await request.formData();
135-
const submission = parse(formData, { schema });
158+
const formData = await request.formData();
159+
const submission = parse(formData, { schema });
136160

137-
if (!submission.value || submission.intent !== "submit") {
138-
return json(submission);
139-
}
161+
if (!submission.value || submission.intent !== "submit") {
162+
return json(submission);
163+
}
140164

141-
try {
142-
const [error, updatedAlert] = await tryCatch(
143-
setBillingAlert(organization.id, {
144-
...submission.value,
145-
amount: submission.value.amount * 100,
146-
})
147-
);
148-
if (error) {
149-
return redirectWithErrorMessage(
150-
v3BillingAlertsPath({ slug: organizationSlug }),
151-
request,
152-
"Failed to update billing alert"
165+
try {
166+
const [error, updatedAlert] = await tryCatch(
167+
setBillingAlert(organization.id, {
168+
...submission.value,
169+
amount: submission.value.amount * 100,
170+
})
153171
);
154-
}
172+
if (error) {
173+
return redirectWithErrorMessage(
174+
v3BillingAlertsPath({ slug: organizationSlug }),
175+
request,
176+
"Failed to update billing alert"
177+
);
178+
}
155179

156-
if (!updatedAlert) {
157-
return redirectWithErrorMessage(
180+
if (!updatedAlert) {
181+
return redirectWithErrorMessage(
182+
v3BillingAlertsPath({ slug: organizationSlug }),
183+
request,
184+
"Failed to update billing alert"
185+
);
186+
}
187+
188+
return redirectWithSuccessMessage(
158189
v3BillingAlertsPath({ slug: organizationSlug }),
159190
request,
160-
"Failed to update billing alert"
191+
"Billing alert updated"
161192
);
193+
} catch (error: any) {
194+
return json({ errors: { body: error.message } }, { status: 400 });
162195
}
163-
164-
return redirectWithSuccessMessage(
165-
v3BillingAlertsPath({ slug: organizationSlug }),
166-
request,
167-
"Billing alert updated"
168-
);
169-
} catch (error: any) {
170-
return json({ errors: { body: error.message } }, { status: 400 });
171196
}
172-
};
197+
);
173198

174199
export default function Page() {
175200
const { alerts } = useTypedLoaderData<typeof loader>();

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { CalendarDaysIcon, CreditCardIcon, StarIcon } from "@heroicons/react/20/solid";
2-
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
32
import { type PlanDefinition } from "@trigger.dev/platform";
43
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
54
import { Feedback } from "~/components/Feedback";
@@ -9,10 +8,10 @@ import { DateTime } from "~/components/primitives/DateTime";
98
import { InfoPanel } from "~/components/primitives/InfoPanel";
109
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
1110
import { Paragraph } from "~/components/primitives/Paragraph";
12-
import { prisma } from "~/db.server";
11+
import { $replica, prisma } from "~/db.server";
1312
import { featuresForRequest } from "~/features.server";
1413
import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
15-
import { requireUserId } from "~/services/session.server";
14+
import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
1615
import {
1716
OrganizationParamsSchema,
1817
organizationPath,
@@ -31,47 +30,78 @@ export const meta: MetaFunction = () => {
3130
];
3231
};
3332

34-
export async function loader({ params, request }: LoaderFunctionArgs) {
35-
const userId = await requireUserId(request);
36-
const { organizationSlug } = OrganizationParamsSchema.parse(params);
33+
async function resolveOrgIdFromSlug(slug: string): Promise<string | null> {
34+
const org = await $replica.organization.findFirst({ where: { slug }, select: { id: true } });
35+
return org?.id ?? null;
36+
}
3737

38-
const { isManagedCloud } = featuresForRequest(request);
39-
if (!isManagedCloud) {
40-
return redirect(organizationPath({ slug: organizationSlug }));
41-
}
38+
export const loader = dashboardLoader(
39+
{
40+
params: OrganizationParamsSchema,
41+
context: async (params) => {
42+
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
43+
return organizationId ? { organizationId } : {};
44+
},
45+
authorization: { action: "manage", resource: { type: "billing" } },
46+
},
47+
async ({ params, request, user }) => {
48+
const userId = user.id;
49+
const { organizationSlug } = params;
50+
51+
const { isManagedCloud } = featuresForRequest(request);
52+
if (!isManagedCloud) {
53+
return redirect(organizationPath({ slug: organizationSlug }));
54+
}
55+
56+
const organization = await prisma.organization.findFirst({
57+
where: { slug: organizationSlug, members: { some: { userId } } },
58+
});
4259

43-
const organization = await prisma.organization.findFirst({
44-
where: { slug: organizationSlug, members: { some: { userId } } },
45-
});
60+
if (!organization) {
61+
throw new Response(null, { status: 404, statusText: "Organization not found" });
62+
}
4663

47-
if (!organization) {
48-
throw new Response(null, { status: 404, statusText: "Organization not found" });
49-
}
64+
const currentPlan = await getCurrentPlan(organization.id);
65+
const showSelfServe = currentPlan?.v3Subscription?.showSelfServe !== false;
5066

51-
const currentPlan = await getCurrentPlan(organization.id);
52-
const showSelfServe = currentPlan?.v3Subscription?.showSelfServe !== false;
67+
//periods
68+
const periodStart = new Date();
69+
periodStart.setUTCHours(0, 0, 0, 0);
70+
periodStart.setUTCDate(1);
5371

54-
//periods
55-
const periodStart = new Date();
56-
periodStart.setUTCHours(0, 0, 0, 0);
57-
periodStart.setUTCDate(1);
72+
const periodEnd = new Date();
73+
periodEnd.setUTCMonth(periodEnd.getMonth() + 1);
74+
periodEnd.setUTCDate(0);
75+
periodEnd.setUTCHours(0, 0, 0, 0);
5876

59-
const periodEnd = new Date();
60-
periodEnd.setUTCMonth(periodEnd.getMonth() + 1);
61-
periodEnd.setUTCDate(0);
62-
periodEnd.setUTCHours(0, 0, 0, 0);
63-
64-
const daysRemaining = Math.ceil(
65-
(periodEnd.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)
66-
);
77+
const daysRemaining = Math.ceil(
78+
(periodEnd.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)
79+
);
6780

68-
// Extract 'message' from search params
69-
const url = new URL(request.url);
70-
const message = url.searchParams.get("message");
81+
// Extract 'message' from search params
82+
const url = new URL(request.url);
83+
const message = url.searchParams.get("message");
84+
85+
if (!showSelfServe) {
86+
return typedjson({
87+
showSelfServe: false as const,
88+
...currentPlan,
89+
organizationSlug,
90+
periodStart,
91+
periodEnd,
92+
daysRemaining,
93+
message,
94+
});
95+
}
96+
97+
const plans = await getPlans();
98+
if (!plans) {
99+
throw new Response(null, { status: 404, statusText: "Plans not found" });
100+
}
71101

72-
if (!showSelfServe) {
73102
return typedjson({
74-
showSelfServe: false as const,
103+
showSelfServe: true as const,
104+
...plans,
75105
...currentPlan,
76106
organizationSlug,
77107
periodStart,
@@ -80,23 +110,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
80110
message,
81111
});
82112
}
83-
84-
const plans = await getPlans();
85-
if (!plans) {
86-
throw new Response(null, { status: 404, statusText: "Plans not found" });
87-
}
88-
89-
return typedjson({
90-
showSelfServe: true as const,
91-
...plans,
92-
...currentPlan,
93-
organizationSlug,
94-
periodStart,
95-
periodEnd,
96-
daysRemaining,
97-
message,
98-
});
99-
}
113+
);
100114

101115
export default function ChoosePlanPage() {
102116
const loaderData = useTypedLoaderData<typeof loader>();

0 commit comments

Comments
 (0)