From 43f2e32231622be0214ef29d51b3282ad4aa7fe9 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 15:30:49 -0700 Subject: [PATCH] Paywall resource policies --- server/lib/billing/tierMatrix.ts | 6 +- server/private/routers/external.ts | 6 +- .../routers/policy/createResourcePolicy.ts | 13 ++ .../routers/policy/deleteResourcePolicy.ts | 2 +- server/private/routers/policy/index.ts | 2 +- .../routers/policy/listResourcePolicies.ts | 2 +- server/routers/resource/updateResource.ts | 41 +++- .../proxy/[niceId]/authentication/page.tsx | 216 +++++++++--------- src/components/ResourcePoliciesTable.tsx | 5 + .../resource-policy/CreatePolicyForm.tsx | 136 ++++++----- 10 files changed, 243 insertions(+), 186 deletions(-) diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index f44cb8bf6..fa0b63a33 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -24,7 +24,8 @@ export enum TierFeature { DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces StandaloneHealthChecks = "standaloneHealthChecks", AlertingRules = "alertingRules", - WildcardSubdomain = "wildcardSubdomain" + WildcardSubdomain = "wildcardSubdomain", + ResourcePolicies = "resourcePolicies" } export const tierMatrix: Record = { @@ -66,5 +67,6 @@ export const tierMatrix: Record = { [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"], [TierFeature.AlertingRules]: ["tier3", "enterprise"], - [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"] + [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.ResourcePolicies]: ["tier3", "enterprise"] }; diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 7e5fc8f67..ba96c4edf 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -389,7 +389,7 @@ authenticated.delete( "/resource-policy/:resourcePolicyId", verifyResourcePolicyAccess, verifyValidLicense, - // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyValidSubscription(tierMatrix.resourcePolicies), verifyLimits, verifyUserHasAction(ActionsEnum.deleteResourcePolicy), logActionAudit(ActionsEnum.deleteResourcePolicy), @@ -399,7 +399,7 @@ authenticated.delete( authenticated.get( "/org/:orgId/resource-policies", verifyValidLicense, - // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyValidSubscription(tierMatrix.resourcePolicies), verifyOrgAccess, verifyLimits, verifyUserHasAction(ActionsEnum.listResourcePolicies), @@ -410,7 +410,7 @@ authenticated.get( authenticated.post( "/org/:orgId/resource-policy", verifyValidLicense, - // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyValidSubscription(tierMatrix.resourcePolicies), verifyOrgAccess, verifyLimits, verifyUserHasAction(ActionsEnum.createResourcePolicy), diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index 48b336f1f..2b4678331 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -1,3 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + import { hashPassword } from "@server/auth/password"; import { db, diff --git a/server/private/routers/policy/deleteResourcePolicy.ts b/server/private/routers/policy/deleteResourcePolicy.ts index 17a9a68f9..a586cf3b4 100644 --- a/server/private/routers/policy/deleteResourcePolicy.ts +++ b/server/private/routers/policy/deleteResourcePolicy.ts @@ -1,7 +1,7 @@ /* * This file is part of a proprietary work. * - * Copyright (c) 2025 Fossorial, Inc. + * Copyright (c) 2025-2026 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. diff --git a/server/private/routers/policy/index.ts b/server/private/routers/policy/index.ts index 1fb73a58c..c780ebfe4 100644 --- a/server/private/routers/policy/index.ts +++ b/server/private/routers/policy/index.ts @@ -1,7 +1,7 @@ /* * This file is part of a proprietary work. * - * Copyright (c) 2025 Fossorial, Inc. + * Copyright (c) 2025-2026 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. diff --git a/server/private/routers/policy/listResourcePolicies.ts b/server/private/routers/policy/listResourcePolicies.ts index 64cd5c61a..beb1b68c3 100644 --- a/server/private/routers/policy/listResourcePolicies.ts +++ b/server/private/routers/policy/listResourcePolicies.ts @@ -1,7 +1,7 @@ /* * This file is part of a proprietary work. * - * Copyright (c) 2025 Fossorial, Inc. + * Copyright (c) 2025-2026 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index d36e49e87..b56469167 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -25,7 +25,10 @@ import { import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; -import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils"; +import { + validateAndConstructDomain, + checkWildcardDomainConflict +} from "@server/lib/domainUtils"; import { build } from "@server/build"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; @@ -304,11 +307,30 @@ async function updateHttpResource( const updateData = parsedBody.data; + const isLicensed = await isLicensedOrSubscribed( + resource.orgId, + tierMatrix.wildcardSubdomain + ); + if (updateData.resourcePolicyId != null) { + if (!isLicensed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Resource policies are not supported on your current plan. Please upgrade to access this feature." + ) + ); + } + const [existingPolicy] = await db .select() .from(resourcePolicies) - .where(eq(resourcePolicies.resourcePolicyId, updateData.resourcePolicyId)) + .where( + eq( + resourcePolicies.resourcePolicyId, + updateData.resourcePolicyId + ) + ) .limit(1); if (!existingPolicy) { @@ -346,10 +368,6 @@ async function updateHttpResource( // Wildcard subdomains are a paid feature if (updateData.subdomain && updateData.subdomain.includes("*")) { - const isLicensed = await isLicensedOrSubscribed( - resource.orgId, - tierMatrix.wildcardSubdomain - ); if (!isLicensed) { return next( createHttpError( @@ -494,10 +512,6 @@ async function updateHttpResource( headers = null; } - const isLicensed = await isLicensedOrSubscribed( - resource.orgId, - tierMatrix.maintencePage - ); if (!isLicensed) { updateData.maintenanceModeEnabled = undefined; updateData.maintenanceModeType = undefined; @@ -560,7 +574,12 @@ async function updateRawResource( const [existingPolicy] = await db .select() .from(resourcePolicies) - .where(eq(resourcePolicies.resourcePolicyId, updateData.resourcePolicyId)) + .where( + eq( + resourcePolicies.resourcePolicyId, + updateData.resourcePolicyId + ) + ) .limit(1); if (!existingPolicy) { diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 4b1e9f516..4d725ae4b 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -41,7 +41,7 @@ import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { build } from "@server/build"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowRightIcon, CheckIcon, ShieldAlertIcon } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -70,6 +70,7 @@ export default function ResourceAuthenticationPage() { const queryClient = useQueryClient(); const { env } = useEnvContext(); + const { isPaidUser } = usePaidStatus(); const api = createApiClient({ env }); const router = useRouter(); @@ -188,111 +189,118 @@ export default function ResourceAuthenticationPage() { return ( <> - {build !== "oss" && ( - - - - {t("resourcePolicySelectTitle")} - - - {t("resourcePolicySelectDescription")} - - - - { - form.setValue("type", value); - }} - cols={2} - /> - {selectedResourceType === "shared" && ( - - - - - - - + + + {t("resourcePolicySelectTitle")} + + + {t("resourcePolicySelectDescription")} + + + + { + form.setValue("type", value); + }} + cols={2} + /> + {selectedResourceType === "shared" && ( + + + + + + + - - {policiesList.map( - (policy) => ( - - setSelectedPolicy( - { - id: policy.resourcePolicyId, - name: policy.name - } - ) - } - > - - {policy.name} - - ) - )} - - - - - - )} - - - - - - )} + value={ + resourcePolicysearchQuery + } + onValueChange={ + setResourcePolicySearchQuery + } + /> + + + {t( + "resourcePolicyNotFound" + )} + + + {policiesList.map( + (policy) => ( + + setSelectedPolicy( + { + id: policy.resourcePolicyId, + name: policy.name + } + ) + } + > + + { + policy.name + } + + ) + )} + + + + + + )} + + + + + + )} {selectedResourceType === "inline" ? ( diff --git a/src/components/ResourcePoliciesTable.tsx b/src/components/ResourcePoliciesTable.tsx index bfdc49724..3039c821c 100644 --- a/src/components/ResourcePoliciesTable.tsx +++ b/src/components/ResourcePoliciesTable.tsx @@ -29,6 +29,8 @@ import { DropdownMenuTrigger } from "./ui/dropdown-menu"; import ConfirmDeleteDialog from "./ConfirmDeleteDialog"; +import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number]; @@ -253,6 +255,9 @@ export function ResourcePoliciesTable({ return ( <> + {selectedResourcePolicy && ( ; } - return ( -
- - {/* Name */} - - - - {t("resourcePolicyName")} - - - {t("resourcePolicyNameDescription")} - - - - - ( - - {t("name")} - - - - - - )} - /> - - - + const policyTiers = tierMatrix[TierFeature.ResourcePolicies]; + const isDisabled = !isPaidUser(policyTiers); - - - - - + return ( + <> + + +
+ + {/* Name */} + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + + {t("name")} + + + + + + + )} + /> + + + + + + + + + +
- + + ); }