From dff45748bd7bac6def2aca8ba2699841aba009ac Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 9 Feb 2026 16:57:41 -0800 Subject: [PATCH] refactor is licensed and subscribed util functions --- server/lib/config.ts | 5 + server/lib/isLicencedOrSubscribed.ts | 7 +- server/lib/isSubscribed.ts | 5 +- server/lib/readConfigFile.ts | 3 +- server/private/lib/checkOrgAccessPolicy.ts | 2 + server/private/lib/isLicencedOrSubscribed.ts | 10 +- server/private/lib/isSubscribed.ts | 8 +- .../private/middlewares/verifySubscription.ts | 75 +-- server/private/routers/external.ts | 2 +- .../[remoteExitNodeId]/credentials/page.tsx | 44 +- .../machine/[niceId]/credentials/page.tsx | 44 +- .../clients/user/[niceId]/general/page.tsx | 444 +++++++++--------- src/app/[orgId]/settings/general/layout.tsx | 15 +- .../settings/general/security/page.tsx | 250 +++++----- .../resources/proxy/[niceId]/general/page.tsx | 21 +- .../sites/[niceId]/credentials/page.tsx | 62 +-- src/app/navigation.tsx | 55 ++- src/components/CreateRoleForm.tsx | 87 ++-- src/components/EditRoleForm.tsx | 88 ++-- src/components/PaidFeaturesAlert.tsx | 7 + src/contexts/subscriptionStatusContext.ts | 1 - src/hooks/usePaidStatus.ts | 25 +- src/lib/pullEnv.ts | 6 +- src/lib/types/env.ts | 1 + src/providers/SubscriptionStatusProvider.tsx | 14 - 25 files changed, 707 insertions(+), 574 deletions(-) diff --git a/server/lib/config.ts b/server/lib/config.ts index d3931ec3..4cd8bbfd 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -107,6 +107,11 @@ export class Config { process.env.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path; } + process.env.DISABLE_ENTERPRISE_FEATURES = parsedConfig.flags + ?.disable_enterprise_features + ? "true" + : "false"; + this.rawConfig = parsedConfig; } diff --git a/server/lib/isLicencedOrSubscribed.ts b/server/lib/isLicencedOrSubscribed.ts index 748bb1b1..a04d44aa 100644 --- a/server/lib/isLicencedOrSubscribed.ts +++ b/server/lib/isLicencedOrSubscribed.ts @@ -1,3 +1,6 @@ -export async function isLicensedOrSubscribed(orgId: string): Promise { +export async function isLicensedOrSubscribed( + orgId: string, + tiers: string[] +): Promise { return false; -} \ No newline at end of file +} diff --git a/server/lib/isSubscribed.ts b/server/lib/isSubscribed.ts index 44a4c0b3..306ab871 100644 --- a/server/lib/isSubscribed.ts +++ b/server/lib/isSubscribed.ts @@ -1,3 +1,6 @@ -export async function isSubscribed(orgId: string): Promise { +export async function isSubscribed( + orgId: string, + tiers: string[] +): Promise { return false; } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 362210ae..bfca5970 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -331,7 +331,8 @@ export const configSchema = z disable_local_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(), - disable_product_help_banners: z.boolean().optional() + disable_product_help_banners: z.boolean().optional(), + disable_enterprise_features: z.boolean().optional() }) .optional(), dns: z diff --git a/server/private/lib/checkOrgAccessPolicy.ts b/server/private/lib/checkOrgAccessPolicy.ts index af318ce0..fee07a62 100644 --- a/server/private/lib/checkOrgAccessPolicy.ts +++ b/server/private/lib/checkOrgAccessPolicy.ts @@ -78,6 +78,8 @@ export async function checkOrgAccessPolicy( } } + // TODO: check that the org is subscribed + // get the needed data if (!props.org) { diff --git a/server/private/lib/isLicencedOrSubscribed.ts b/server/private/lib/isLicencedOrSubscribed.ts index 3b2f6592..3f8d2a6d 100644 --- a/server/private/lib/isLicencedOrSubscribed.ts +++ b/server/private/lib/isLicencedOrSubscribed.ts @@ -13,16 +13,18 @@ import { build } from "@server/build"; import license from "#private/license/license"; -import { getOrgTierData } from "#private/lib/billing"; +import { isSubscribed } from "#private/lib/isSubscribed"; -export async function isLicensedOrSubscribed(orgId: string): Promise { +export async function isLicensedOrSubscribed( + orgId: string, + tiers: string[] +): Promise { if (build === "enterprise") { return await license.isUnlocked(); } if (build === "saas") { - const { tier, active } = await getOrgTierData(orgId); - return (tier == "tier1" || tier == "tier2" || tier == "tier3") && active; + return isSubscribed(orgId, tiers); } return false; diff --git a/server/private/lib/isSubscribed.ts b/server/private/lib/isSubscribed.ts index 12bbb965..23ffc698 100644 --- a/server/private/lib/isSubscribed.ts +++ b/server/private/lib/isSubscribed.ts @@ -14,10 +14,14 @@ import { build } from "@server/build"; import { getOrgTierData } from "#private/lib/billing"; -export async function isSubscribed(orgId: string): Promise { +export async function isSubscribed( + orgId: string, + tiers: string[] +): Promise { if (build === "saas") { const { tier, active } = await getOrgTierData(orgId); - return (tier == "tier1" || tier == "tier2" || tier == "tier3") && active; + const isTier = (tier && tiers.includes(tier)) || false; + return active && isTier; } return false; diff --git a/server/private/middlewares/verifySubscription.ts b/server/private/middlewares/verifySubscription.ts index fec9241a..3ab351a1 100644 --- a/server/private/middlewares/verifySubscription.ts +++ b/server/private/middlewares/verifySubscription.ts @@ -17,44 +17,51 @@ import HttpCode from "@server/types/HttpCode"; import { build } from "@server/build"; import { getOrgTierData } from "#private/lib/billing"; -export async function verifyValidSubscription( - req: Request, - res: Response, - next: NextFunction -) { - try { - if (build != "saas") { +export function verifyValidSubscription(tiers: string[]) { + return async function ( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + if (build != "saas") { + return next(); + } + + const orgId = + req.params.orgId || + req.body.orgId || + req.query.orgId || + req.userOrgId; + + if (!orgId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Organization ID is required to verify subscription" + ) + ); + } + + const { tier, active } = await getOrgTierData(orgId); + const isTier = tiers.includes(tier || ""); + if (!isTier || !active) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Organization does not have an active subscription" + ) + ); + } + return next(); - } - - const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId; - - if (!orgId) { + } catch (e) { return next( createHttpError( - HttpCode.BAD_REQUEST, - "Organization ID is required to verify subscription" + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying subscription" ) ); } - - const { tier, active } = await getOrgTierData(orgId); - if ((tier == "tier1" || tier == "tier2" || tier == "tier3") && active) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Organization does not have an active subscription" - ) - ); - } - - return next(); - } catch (e) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying subscription" - ) - ); - } + }; } diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index bef493ca..37048c34 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -86,7 +86,7 @@ authenticated.put( authenticated.post( "/org/:orgId/idp/:idpId/oidc", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(), verifyOrgAccess, verifyIdpAccess, verifyUserHasAction(ActionsEnum.updateIdp), diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx index aff31f46..2fa2b753 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx @@ -195,27 +195,29 @@ export default function CredentialsPage() { )} - - - - + {!env.flags.disableEnterpriseFeatures && ( + + + + + )} diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx index e6b5ff20..024f4cd7 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx @@ -183,27 +183,29 @@ export default function CredentialsPage() { )} - - - - + {!env.flags.disableEnterpriseFeatures && ( + + + + + )} (null); const [isRefreshing, setIsRefreshing] = useState(false); const [, startTransition] = useTransition(); + const { env } = useEnvContext(); const showApprovalFeatures = build !== "oss" && isPaidUser; @@ -567,231 +568,246 @@ export default function GeneralPage() { )} - - - - {t("deviceSecurity")} - - - {t("deviceSecurityDescription")} - - + {!env.flags.disableEnterpriseFeatures && ( + + + + {t("deviceSecurity")} + + + {t("deviceSecurityDescription")} + + - - - {client.posture && - Object.keys(client.posture).length > 0 ? ( - <> - - {client.posture.biometricsEnabled !== null && - client.posture.biometricsEnabled !== - undefined && ( - - - {t("biometricsEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .biometricsEnabled - ) - : "-"} - - - )} + + + {client.posture && + Object.keys(client.posture).length > 0 ? ( + <> + + {client.posture.biometricsEnabled !== + null && + client.posture.biometricsEnabled !== + undefined && ( + + + {t("biometricsEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .biometricsEnabled + ) + : "-"} + + + )} - {client.posture.diskEncrypted !== null && - client.posture.diskEncrypted !== - undefined && ( - - - {t("diskEncrypted")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .diskEncrypted - ) - : "-"} - - - )} + {client.posture.diskEncrypted !== null && + client.posture.diskEncrypted !== + undefined && ( + + + {t("diskEncrypted")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .diskEncrypted + ) + : "-"} + + + )} - {client.posture.firewallEnabled !== null && - client.posture.firewallEnabled !== - undefined && ( - - - {t("firewallEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .firewallEnabled - ) - : "-"} - - - )} + {client.posture.firewallEnabled !== null && + client.posture.firewallEnabled !== + undefined && ( + + + {t("firewallEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .firewallEnabled + ) + : "-"} + + + )} - {client.posture.autoUpdatesEnabled !== null && - client.posture.autoUpdatesEnabled !== - undefined && ( - - - {t("autoUpdatesEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .autoUpdatesEnabled - ) - : "-"} - - - )} + {client.posture.autoUpdatesEnabled !== + null && + client.posture.autoUpdatesEnabled !== + undefined && ( + + + {t("autoUpdatesEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .autoUpdatesEnabled + ) + : "-"} + + + )} - {client.posture.tpmAvailable !== null && - client.posture.tpmAvailable !== - undefined && ( - - - {t("tpmAvailable")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .tpmAvailable - ) - : "-"} - - - )} + {client.posture.tpmAvailable !== null && + client.posture.tpmAvailable !== + undefined && ( + + + {t("tpmAvailable")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .tpmAvailable + ) + : "-"} + + + )} - {client.posture.windowsAntivirusEnabled !== - null && - client.posture.windowsAntivirusEnabled !== - undefined && ( - - - {t("windowsAntivirusEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .windowsAntivirusEnabled - ) - : "-"} - - - )} + {client.posture.windowsAntivirusEnabled !== + null && + client.posture + .windowsAntivirusEnabled !== + undefined && ( + + + {t( + "windowsAntivirusEnabled" + )} + + + {isPaidUser + ? formatPostureValue( + client.posture + .windowsAntivirusEnabled + ) + : "-"} + + + )} - {client.posture.macosSipEnabled !== null && - client.posture.macosSipEnabled !== - undefined && ( - - - {t("macosSipEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .macosSipEnabled - ) - : "-"} - - - )} + {client.posture.macosSipEnabled !== null && + client.posture.macosSipEnabled !== + undefined && ( + + + {t("macosSipEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .macosSipEnabled + ) + : "-"} + + + )} - {client.posture.macosGatekeeperEnabled !== - null && - client.posture.macosGatekeeperEnabled !== - undefined && ( - - - {t("macosGatekeeperEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .macosGatekeeperEnabled - ) - : "-"} - - - )} + {client.posture.macosGatekeeperEnabled !== + null && + client.posture + .macosGatekeeperEnabled !== + undefined && ( + + + {t( + "macosGatekeeperEnabled" + )} + + + {isPaidUser + ? formatPostureValue( + client.posture + .macosGatekeeperEnabled + ) + : "-"} + + + )} - {client.posture.macosFirewallStealthMode !== - null && - client.posture.macosFirewallStealthMode !== - undefined && ( - - - {t("macosFirewallStealthMode")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .macosFirewallStealthMode - ) - : "-"} - - - )} + {client.posture.macosFirewallStealthMode !== + null && + client.posture + .macosFirewallStealthMode !== + undefined && ( + + + {t( + "macosFirewallStealthMode" + )} + + + {isPaidUser + ? formatPostureValue( + client.posture + .macosFirewallStealthMode + ) + : "-"} + + + )} - {client.posture.linuxAppArmorEnabled !== null && - client.posture.linuxAppArmorEnabled !== - undefined && ( - - - {t("linuxAppArmorEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .linuxAppArmorEnabled - ) - : "-"} - - - )} + {client.posture.linuxAppArmorEnabled !== + null && + client.posture.linuxAppArmorEnabled !== + undefined && ( + + + {t("linuxAppArmorEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .linuxAppArmorEnabled + ) + : "-"} + + + )} - {client.posture.linuxSELinuxEnabled !== null && - client.posture.linuxSELinuxEnabled !== - undefined && ( - - - {t("linuxSELinuxEnabled")} - - - {isPaidUser - ? formatPostureValue( - client.posture - .linuxSELinuxEnabled - ) - : "-"} - - - )} - - - ) : ( -
- {t("noData")} -
- )} -
-
+ {client.posture.linuxSELinuxEnabled !== + null && + client.posture.linuxSELinuxEnabled !== + undefined && ( + + + {t("linuxSELinuxEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .linuxSELinuxEnabled + ) + : "-"} + + + )} + + + ) : ( +
+ {t("noData")} +
+ )} + +
+ )} ); } diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index a3f7264f..b69969f4 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -10,6 +10,7 @@ import { getTranslations } from "next-intl/server"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; import { build } from "@server/build"; +import { pullEnv } from "@app/lib/pullEnv"; type GeneralSettingsProps = { children: React.ReactNode; @@ -23,6 +24,7 @@ export default async function GeneralSettingsPage({ const { orgId } = await params; const user = await verifySession(); + const env = pullEnv(); if (!user) { redirect(`/`); @@ -56,10 +58,15 @@ export default async function GeneralSettingsPage({ title: t("security"), href: `/{orgId}/settings/general/security` }, - { - title: t("authPage"), - href: `/{orgId}/settings/general/auth-page` - } + // PaidFeaturesAlert + ...(!env.flags.disableEnterpriseFeatures + ? [ + { + title: t("authPage"), + href: `/{orgId}/settings/general/auth-page` + } + ] + : []) ]; return ( diff --git a/src/app/[orgId]/settings/general/security/page.tsx b/src/app/[orgId]/settings/general/security/page.tsx index 47946415..55aa9d57 100644 --- a/src/app/[orgId]/settings/general/security/page.tsx +++ b/src/app/[orgId]/settings/general/security/page.tsx @@ -102,10 +102,13 @@ type SectionFormProps = { export default function SecurityPage() { const { org } = useOrgContext(); + const { env } = useEnvContext(); return ( - + {!env.flags.disableEnterpriseFeatures && ( + + )} ); } @@ -135,7 +138,8 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { const { isPaidUser, hasSaasSubscription } = usePaidStatus(); const [, formAction, loadingSave] = useActionState(performSave, null); - const api = createApiClient(useEnvContext()); + const { env } = useEnvContext(); + const api = createApiClient({ env }); async function performSave() { const isValid = await form.trigger(); @@ -238,120 +242,144 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { )} /> - + {!env.flags.disableEnterpriseFeatures && ( + <> + - { - const isDisabled = !isPaidUser; + { + const isDisabled = !isPaidUser; - return ( - - - {t("logRetentionAccessLabel")} - - - - - - - ); - }} - /> - { - const isDisabled = !isPaidUser; - - return ( - - - {t("logRetentionActionLabel")} - - - { + if ( + !isDisabled + ) { + field.onChange( + parseInt( + value, + 10 + ) + ); + } + }} + disabled={ + isDisabled + } + > + + - ) + /> + + + {LOG_RETENTION_OPTIONS.map( + ( + option + ) => ( + + {t( + option.label + )} + + ) + )} + + + + + + ); + }} + /> + { + const isDisabled = !isPaidUser; + + return ( + + + {t( + "logRetentionActionLabel" )} - - - - - - ); - }} - /> + + + + + + + ); + }} + /> + + )} diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index b4dc3ad8..1ed8eb17 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -163,7 +163,9 @@ function MaintenanceSectionForm({ const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); const isSaasNotSubscribed = build === "saas" && !subscription?.isSubscribed(); - return isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"; + return ( + isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss" + ); }; if (!resource.http) { @@ -189,13 +191,14 @@ function MaintenanceSectionForm({ className="space-y-4" id="maintenance-settings-form" > - + { const isDisabled = - isSecurityFeatureDisabled() || resource.http === false; + isSecurityFeatureDisabled() || + resource.http === false; return ( @@ -415,7 +418,7 @@ function MaintenanceSectionForm({ - - + {!env.flags.disableEnterpriseFeatures && ( + + + + + )} )} - - - + {!env.flags.disableEnterpriseFeatures && ( + + + + )} )} diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 854f6e19..d74ef30b 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -121,16 +121,27 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [ href: "/{orgId}/settings/access/roles", icon: }, - { - title: "sidebarIdentityProviders", - href: "/{orgId}/settings/idp", - icon: - }, - { - title: "sidebarApprovals", - href: "/{orgId}/settings/access/approvals", - icon: - }, + // PaidFeaturesAlert + ...((build === "oss" && !env?.flags.disableEnterpriseFeatures) || + build === "saas" || + env?.flags.useOrgOnlyIdp + ? [ + { + title: "sidebarIdentityProviders", + href: "/{orgId}/settings/idp", + icon: + } + ] + : []), + ...(!env?.flags.disableEnterpriseFeatures + ? [ + { + title: "sidebarApprovals", + href: "/{orgId}/settings/access/approvals", + icon: + } + ] + : []), { title: "sidebarShareableLinks", href: "/{orgId}/settings/share-links", @@ -147,16 +158,20 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [ href: "/{orgId}/settings/logs/request", icon: }, - { - title: "sidebarLogsAccess", - href: "/{orgId}/settings/logs/access", - icon: - }, - { - title: "sidebarLogsAction", - href: "/{orgId}/settings/logs/action", - icon: - } + ...(!env?.flags.disableEnterpriseFeatures + ? [ + { + title: "sidebarLogsAccess", + href: "/{orgId}/settings/logs/access", + icon: + }, + { + title: "sidebarLogsAction", + href: "/{orgId}/settings/logs/action", + icon: + } + ] + : []) ]; const analytics = { diff --git a/src/components/CreateRoleForm.tsx b/src/components/CreateRoleForm.tsx index 3ea56c53..2642cf64 100644 --- a/src/components/CreateRoleForm.tsx +++ b/src/components/CreateRoleForm.tsx @@ -51,6 +51,7 @@ export default function CreateRoleForm({ const { org } = useOrgContext(); const t = useTranslations(); const { isPaidUser } = usePaidStatus(); + const { env } = useEnvContext(); const formSchema = z.object({ name: z @@ -161,50 +162,56 @@ export default function CreateRoleForm({ )} /> - + {!env.flags.disableEnterpriseFeatures && ( + <> + - ( - - - { - if ( - checked !== - "indeterminate" - ) { - form.setValue( - "requireDeviceApproval", + ( + + + - + ) => { + if ( + checked !== + "indeterminate" + ) { + form.setValue( + "requireDeviceApproval", + checked + ); + } + }} + label={t( + "requireDeviceApproval" + )} + /> + - - {t( - "requireDeviceApprovalDescription" - )} - + + {t( + "requireDeviceApprovalDescription" + )} + - - - )} - /> + + + )} + /> + + )} diff --git a/src/components/EditRoleForm.tsx b/src/components/EditRoleForm.tsx index 81b5bef5..1feb95c6 100644 --- a/src/components/EditRoleForm.tsx +++ b/src/components/EditRoleForm.tsx @@ -59,6 +59,7 @@ export default function EditRoleForm({ const { org } = useOrgContext(); const t = useTranslations(); const { isPaidUser } = usePaidStatus(); + const { env } = useEnvContext(); const formSchema = z.object({ name: z @@ -168,50 +169,57 @@ export default function EditRoleForm({ )} /> - - ( - - - { - if ( - checked !== - "indeterminate" - ) { - form.setValue( - "requireDeviceApproval", + {!env.flags.disableEnterpriseFeatures && ( + <> + + + ( + + + - + ) => { + if ( + checked !== + "indeterminate" + ) { + form.setValue( + "requireDeviceApproval", + checked + ); + } + }} + label={t( + "requireDeviceApproval" + )} + /> + - - {t( - "requireDeviceApprovalDescription" - )} - + + {t( + "requireDeviceApprovalDescription" + )} + - - - )} - /> + + + )} + /> + + )} diff --git a/src/components/PaidFeaturesAlert.tsx b/src/components/PaidFeaturesAlert.tsx index 4fd1d0de..b2c96ab8 100644 --- a/src/components/PaidFeaturesAlert.tsx +++ b/src/components/PaidFeaturesAlert.tsx @@ -5,6 +5,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { ExternalLink, KeyRound, Sparkles } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const bannerClassName = "mb-6 border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden"; @@ -15,6 +16,12 @@ const bannerRowClassName = export function PaidFeaturesAlert() { const t = useTranslations(); const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus(); + const { env } = useEnvContext(); + + if (env.flags.disableEnterpriseFeatures) { + return null; + } + return ( <> {build === "saas" && !hasSaasSubscription ? ( diff --git a/src/contexts/subscriptionStatusContext.ts b/src/contexts/subscriptionStatusContext.ts index 71fe7004..a3efc67f 100644 --- a/src/contexts/subscriptionStatusContext.ts +++ b/src/contexts/subscriptionStatusContext.ts @@ -4,7 +4,6 @@ import { createContext } from "react"; type SubscriptionStatusContextType = { subscriptionStatus: GetOrgSubscriptionResponse | null; updateSubscriptionStatus: (updatedSite: GetOrgSubscriptionResponse) => void; - isActive: () => boolean; getTier: () => { tier: string | null; active: boolean }; isSubscribed: () => boolean; subscribed: boolean; diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts index d8173e6e..88423853 100644 --- a/src/hooks/usePaidStatus.ts +++ b/src/hooks/usePaidStatus.ts @@ -8,14 +8,29 @@ export function usePaidStatus() { // Check if features are disabled due to licensing/subscription const hasEnterpriseLicense = build === "enterprise" && isUnlocked(); - const hasSaasSubscription = - build === "saas" && - subscription?.isSubscribed() && - subscription.isActive(); + const tierData = subscription?.getTier(); + const hasSaasSubscription = build === "saas" && tierData?.active; + + function isPaidUser(tiers: string[]): boolean { + if (hasEnterpriseLicense) { + return true; + } + + if ( + hasSaasSubscription && + tierData?.tier && + tiers.includes(tierData.tier) + ) { + return true; + } + + return false; + } return { hasEnterpriseLicense, hasSaasSubscription, - isPaidUser: hasEnterpriseLicense || hasSaasSubscription + isPaidUser, + subscriptionTier: tierData?.tier }; } diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts index 319e08f0..298745b9 100644 --- a/src/lib/pullEnv.ts +++ b/src/lib/pullEnv.ts @@ -65,7 +65,11 @@ export function pullEnv(): Env { ? true : false, useOrgOnlyIdp: - process.env.USE_ORG_ONLY_IDP === "true" ? true : false + process.env.USE_ORG_ONLY_IDP === "true" ? true : false, + disableEnterpriseFeatures: + process.env.DISABLE_ENTERPRISE_FEATURES === "true" + ? true + : false }, branding: { diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts index f99e1994..925e4348 100644 --- a/src/lib/types/env.ts +++ b/src/lib/types/env.ts @@ -35,6 +35,7 @@ export type Env = { usePangolinDns: boolean; disableProductHelpBanners: boolean; useOrgOnlyIdp: boolean; + disableEnterpriseFeatures: boolean; }; branding: { appName?: string; diff --git a/src/providers/SubscriptionStatusProvider.tsx b/src/providers/SubscriptionStatusProvider.tsx index fad6469d..be5a7b5a 100644 --- a/src/providers/SubscriptionStatusProvider.tsx +++ b/src/providers/SubscriptionStatusProvider.tsx @@ -31,16 +31,6 @@ export function SubscriptionStatusProvider({ }); }; - const isActive = () => { - if (subscriptionStatus?.subscriptions) { - // Check if any subscription is active - return subscriptionStatus.subscriptions.some( - (sub) => sub.subscription?.status === "active" - ); - } - return false; - }; - const getTier = () => { if (subscriptionStatus?.subscriptions) { // Iterate through all subscriptions @@ -65,9 +55,6 @@ export function SubscriptionStatusProvider({ }; const isSubscribed = () => { - if (build === "enterprise") { - return true; - } const { tier, active } = getTier(); return ( (tier == "tier1" || tier == "tier2" || tier == "tier3") && @@ -82,7 +69,6 @@ export function SubscriptionStatusProvider({ value={{ subscriptionStatus: subscriptionStatusState, updateSubscriptionStatus, - isActive, getTier, isSubscribed, subscribed