Skip to content

Commit 549c910

Browse files
committed
feat(webapp): gate billing limits on a dedicated permission
The billing limits page and the usage-limit banners (their configure and resolve actions) now check a manage:billing-limits permission instead of the broader billing permission. Access to billing limits can be granted to a role independently of subscription and payment management.
1 parent d720690 commit 549c910

5 files changed

Lines changed: 18 additions & 16 deletions

File tree

apps/webapp/app/components/billing/OrgBanner.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
useOptionalOrganization,
1010
useOrganization,
1111
useBillingLimit,
12-
useCanManageBilling,
12+
useCanManageBillingLimits,
1313
} from "~/hooks/useOrganizations";
1414
import { useOptionalProject, useProject } from "~/hooks/useProject";
1515
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
@@ -80,8 +80,8 @@ export function OrgBanner() {
8080
function LimitRejectedBanner() {
8181
const organization = useOrganization();
8282
const showSelfServe = useShowSelfServe();
83-
const canManageBilling = useCanManageBilling();
84-
const canResolve = showSelfServe && canManageBilling;
83+
const canManageBillingLimits = useCanManageBillingLimits();
84+
const canResolve = showSelfServe && canManageBillingLimits;
8585

8686
return (
8787
<AnimatedOrgBannerBar
@@ -110,8 +110,8 @@ function LimitGraceBanner() {
110110
const organization = useOrganization();
111111
const billingLimit = useBillingLimit();
112112
const showSelfServe = useShowSelfServe();
113-
const canManageBilling = useCanManageBilling();
114-
const canResolve = showSelfServe && canManageBilling;
113+
const canManageBillingLimits = useCanManageBillingLimits();
114+
const canResolve = showSelfServe && canManageBillingLimits;
115115

116116
const graceEndsAt =
117117
billingLimit?.isConfigured && billingLimit.limitState.status === "grace"
@@ -143,21 +143,21 @@ function LimitGraceBanner() {
143143

144144
function NoLimitConfiguredBanner() {
145145
const organization = useOrganization();
146-
const canManageBilling = useCanManageBilling();
146+
const canManageBillingLimits = useCanManageBillingLimits();
147147

148148
return (
149149
<AnimatedOrgBannerBar
150150
show
151151
variant="warning"
152152
action={
153-
canManageBilling ? (
153+
canManageBillingLimits ? (
154154
<LinkButton variant="tertiary/small" to={v3BillingLimitsPath(organization)}>
155155
Configure billing limit
156156
</LinkButton>
157157
) : undefined
158158
}
159159
>
160-
{canManageBilling
160+
{canManageBillingLimits
161161
? "Protect your organization from unexpected usage spikes."
162162
: "Billing limits are not configured for this organization. Contact an organization administrator to configure them."}
163163
</AnimatedOrgBannerBar>

apps/webapp/app/hooks/useOrganizations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ export function useBillingLimit(matches?: UIMatch[]) {
9696
return data?.billingLimit;
9797
}
9898

99-
export function useCanManageBilling(matches?: UIMatch[]) {
99+
export function useCanManageBillingLimits(matches?: UIMatch[]) {
100100
const data = useTypedMatchesData<typeof orgLoader>({
101101
id: "routes/_app.orgs.$organizationSlug",
102102
matches,
103103
});
104-
return data?.canManageBilling === true;
104+
return data?.canManageBillingLimits === true;
105105
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ import {
7777

7878
const billingLimitsAuthorization = {
7979
action: "manage" as const,
80-
resource: { type: "billing" as const },
80+
resource: { type: "billing-limits" as const },
8181
};
8282

8383
export const meta: MetaFunction = () => {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { RegionsPresenter, type Region } from "~/presenters/v3/RegionsPresenter.
1111
import { getImpersonationId } from "~/services/impersonation.server";
1212
import { getCachedUsage, getBillingLimit, getCurrentPlan } from "~/services/platform.v3.server";
1313
import { rbac } from "~/services/rbac.server";
14-
import { canManageBilling } from "~/services/routeBuilders/permissions.server";
14+
import { canManageBillingLimits } from "~/services/routeBuilders/permissions.server";
1515
import { requireUser } from "~/services/session.server";
1616
import { telemetry } from "~/services/telemetry.server";
1717
import { organizationPath } from "~/utils/pathBuilder";
@@ -124,7 +124,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
124124
.catch(() => [] as Region[])
125125
: Promise.resolve([] as Region[]),
126126
]);
127-
const userCanManageBilling = sessionAuth.ok ? canManageBilling(sessionAuth.ability) : false;
127+
const userCanManageBillingLimits = sessionAuth.ok
128+
? canManageBillingLimits(sessionAuth.ability)
129+
: false;
128130

129131
let hasExceededFreeTier = false;
130132
let usagePercentage = 0;
@@ -181,7 +183,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
181183
limit: dashboardLimit,
182184
},
183185
widgetLimitPerDashboard,
184-
canManageBilling: userCanManageBilling,
186+
canManageBillingLimits: userCanManageBillingLimits,
185187
});
186188
};
187189

apps/webapp/app/services/routeBuilders/permissions.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export type PermissionCheck =
1919
* returned booleans are display-only: the route builder's `authorization`
2020
* block is the real security boundary.
2121
*/
22-
export function canManageBilling(ability: RbacAbility): boolean {
23-
return ability.can("manage", { type: "billing" });
22+
export function canManageBillingLimits(ability: RbacAbility): boolean {
23+
return ability.can("manage", { type: "billing-limits" });
2424
}
2525

2626
export function checkPermissions<K extends string>(

0 commit comments

Comments
 (0)