diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index 2065398f2..45434aac4 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -16,18 +16,18 @@ export enum TierFeature { SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning - SshPam = "sshPam", FullRbac = "fullRbac", SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed SIEM = "siem", // handle downgrade by disabling SIEM integrations - HTTPPrivateResources = "httpPrivateResources", // handle downgrade by disabling HTTP private resources DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces StandaloneHealthChecks = "standaloneHealthChecks", AlertingRules = "alertingRules", WildcardSubdomain = "wildcardSubdomain", Labels = "labels", NewtAutoUpdate = "newtAutoUpdate", - ResourcePolicies = "resourcePolicies" + ResourcePolicies = "resourcePolicies", + AdvancedPublicResources = "advancedPublicResources", + AdvancedPrivateResources = "advancedPrivateResources" } export const tierMatrix: Record = { @@ -62,15 +62,25 @@ export const tierMatrix: Record = { "enterprise" ], [TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"], - [TierFeature.SshPam]: ["tier1", "tier3", "enterprise"], [TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"], [TierFeature.SIEM]: ["enterprise"], - [TierFeature.HTTPPrivateResources]: ["tier3", "enterprise"], [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"], [TierFeature.AlertingRules]: ["tier3", "enterprise"], [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"], - [TierFeature.ResourcePolicies]: ["tier3", "enterprise"] + [TierFeature.ResourcePolicies]: ["tier3", "enterprise"], + [TierFeature.AdvancedPublicResources]: [ + "tier1", + "tier2", + "tier3", + "enterprise" + ], + [TierFeature.AdvancedPrivateResources]: [ + "tier1", + "tier2", + "tier3", + "enterprise" + ] }; diff --git a/server/private/routers/billing/featureLifecycle.ts b/server/private/routers/billing/featureLifecycle.ts index 6cb98ba5d..75d11c756 100644 --- a/server/private/routers/billing/featureLifecycle.ts +++ b/server/private/routers/billing/featureLifecycle.ts @@ -308,8 +308,8 @@ async function disableFeature( await disableAutoProvisioning(orgId); break; - case TierFeature.SshPam: - await disableSshPam(orgId); + case TierFeature.AdvancedPrivateResources: + await disableAdvancedPrivateResources(orgId); break; case TierFeature.FullRbac: @@ -357,10 +357,11 @@ async function disableDeviceApprovals(orgId: string): Promise { logger.info(`Disabled device approvals on all roles for org ${orgId}`); } -async function disableSshPam(orgId: string): Promise { - logger.info( - `Disabled SSH PAM options on all roles and site resources for org ${orgId}` - ); +async function disableAdvancedPrivateResources(orgId: string): Promise { + // TODO: implement logic to disable advanced private resourcs like ssh and ssh pam + // logger.info( + // `Disabled advanced private resources on all roles and site resources for org ${orgId}` + // ); } async function disableFullRbac(orgId: string): Promise { diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 82d63d7dd..0598a1514 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -610,7 +610,7 @@ authenticated.put( authenticated.post( "/org/:orgId/ssh/sign-key", verifyValidLicense, - verifyValidSubscription(tierMatrix.sshPam), + verifyValidSubscription(tierMatrix.advancedPrivateResources), verifyOrgAccess, verifyLimits, verifyUserHasAction(ActionsEnum.signSshKey), diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts index efddfc0d9..72152108a 100644 --- a/server/private/routers/ssh/signSshKey.ts +++ b/server/private/routers/ssh/signSshKey.ts @@ -149,7 +149,7 @@ export async function signSshKey( const isLicensed = await isLicensedOrSubscribed( orgId, - tierMatrix.sshPam + tierMatrix.advancedPrivateResources ); if (!isLicensed) { return next( diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 44af04f7c..0bb90e7b8 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -31,7 +31,7 @@ import { } from "@server/lib/domainUtils"; import { isSubscribed } from "#dynamic/lib/isSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { getUniqueResourceName, getUniqueResourcePolicyName @@ -342,6 +342,21 @@ async function createHttpResource( } } + if ( + ["ssh", "rdp", "vnc"].includes(mode!) && + !isLicensedOrSubscribed( + orgId!, + tierMatrix[TierFeature.AdvancedPublicResources] + ) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Your current subscription does not support browser gateway resources. Please upgrade to access this feature." + ) + ); + } + // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( domainId, diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index d7aceb743..e193c5018 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -123,23 +123,40 @@ export async function createRole( ); } - const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals); + const isLicensedDeviceApprovals = await isLicensedOrSubscribed( + orgId, + tierMatrix.deviceApprovals + ); if (!isLicensedDeviceApprovals) { roleData.requireDeviceApproval = undefined; } - const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam); + const isLicensedSshPam = await isLicensedOrSubscribed( + orgId, + tierMatrix.advancedPrivateResources + ); const roleInsertValues: Record = { name: roleData.name, orgId }; - if (roleData.description !== undefined) roleInsertValues.description = roleData.description; - if (roleData.requireDeviceApproval !== undefined) roleInsertValues.requireDeviceApproval = roleData.requireDeviceApproval; + if (roleData.description !== undefined) + roleInsertValues.description = roleData.description; + if (roleData.requireDeviceApproval !== undefined) + roleInsertValues.requireDeviceApproval = + roleData.requireDeviceApproval; if (isLicensedSshPam) { - if (roleData.sshSudoMode !== undefined) roleInsertValues.sshSudoMode = roleData.sshSudoMode; - if (roleData.sshSudoCommands !== undefined) roleInsertValues.sshSudoCommands = JSON.stringify(roleData.sshSudoCommands); - if (roleData.sshCreateHomeDir !== undefined) roleInsertValues.sshCreateHomeDir = roleData.sshCreateHomeDir; - if (roleData.sshUnixGroups !== undefined) roleInsertValues.sshUnixGroups = JSON.stringify(roleData.sshUnixGroups); + if (roleData.sshSudoMode !== undefined) + roleInsertValues.sshSudoMode = roleData.sshSudoMode; + if (roleData.sshSudoCommands !== undefined) + roleInsertValues.sshSudoCommands = JSON.stringify( + roleData.sshSudoCommands + ); + if (roleData.sshCreateHomeDir !== undefined) + roleInsertValues.sshCreateHomeDir = roleData.sshCreateHomeDir; + if (roleData.sshUnixGroups !== undefined) + roleInsertValues.sshUnixGroups = JSON.stringify( + roleData.sshUnixGroups + ); } await db.transaction(async (trx) => { diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts index 29f43850f..eb3239419 100644 --- a/server/routers/role/updateRole.ts +++ b/server/routers/role/updateRole.ts @@ -134,12 +134,18 @@ export async function updateRole( ); } - const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals); + const isLicensedDeviceApprovals = await isLicensedOrSubscribed( + orgId, + tierMatrix.deviceApprovals + ); if (!isLicensedDeviceApprovals) { updateData.requireDeviceApproval = undefined; } - const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam); + const isLicensedSshPam = await isLicensedOrSubscribed( + orgId, + tierMatrix.advancedPrivateResources + ); if (!isLicensedSshPam) { delete updateData.sshSudoMode; delete updateData.sshSudoCommands; @@ -147,10 +153,14 @@ export async function updateRole( delete updateData.sshUnixGroups; } else { if (Array.isArray(updateData.sshSudoCommands)) { - updateData.sshSudoCommands = JSON.stringify(updateData.sshSudoCommands); + updateData.sshSudoCommands = JSON.stringify( + updateData.sshSudoCommands + ); } if (Array.isArray(updateData.sshUnixGroups)) { - updateData.sshUnixGroups = JSON.stringify(updateData.sshUnixGroups); + updateData.sshUnixGroups = JSON.stringify( + updateData.sshUnixGroups + ); } } diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 0d012bf25..3f38cc7e1 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -293,7 +293,7 @@ export async function createSiteResource( if (mode == "http") { const hasHttpFeature = await isLicensedOrSubscribed( orgId, - tierMatrix[TierFeature.HTTPPrivateResources] + tierMatrix[TierFeature.AdvancedPrivateResources] ); if (!hasHttpFeature) { return next( @@ -425,9 +425,18 @@ export async function createSiteResource( const isLicensedSshPam = await isLicensedOrSubscribed( orgId, - tierMatrix.sshPam + tierMatrix.advancedPrivateResources ); + if (mode == "ssh" && !isLicensedSshPam) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "SSH private resources are not included in your current plan. Please upgrade." + ) + ); + } + let updatedNiceId = niceId; if (!niceId) { updatedNiceId = await getUniqueSiteResourceName(orgId); diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index ffdf51f35..d503a2b5c 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -314,7 +314,7 @@ export async function updateSiteResource( if (mode == "http") { const hasHttpFeature = await isLicensedOrSubscribed( existingSiteResource.orgId, - tierMatrix[TierFeature.HTTPPrivateResources] + tierMatrix[TierFeature.AdvancedPrivateResources] ); if (!hasHttpFeature) { return next( @@ -328,7 +328,7 @@ export async function updateSiteResource( const isLicensedSshPam = await isLicensedOrSubscribed( existingSiteResource.orgId, - tierMatrix.sshPam + tierMatrix.advancedPrivateResources ); const [org] = await db diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx index defd4891a..ee564156a 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx @@ -10,11 +10,14 @@ import { SettingsSectionTitle } from "@app/components/Settings"; import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { type Selectedsite } from "@app/components/site-selector"; import { Button } from "@app/components/ui/button"; import { toast } from "@app/hooks/useToast"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import { createApiClient } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api/formatAxiosError"; import { useQuery } from "@tanstack/react-query"; @@ -48,13 +51,21 @@ export default function SshSettingsPage(props: { }) { const params = use(props.params); const { resource, updateResource } = useResourceContext(); + const { isPaidUser } = usePaidStatus(); + const disabled = !isPaidUser( + tierMatrix[TierFeature.AdvancedPublicResources] + ); return ( + ); @@ -63,11 +74,13 @@ export default function SshSettingsPage(props: { function SshServerForm({ orgId, resource, - updateResource + updateResource, + disabled }: { orgId: string; resource: GetResourceResponse; updateResource: ResourceContextType["updateResource"]; + disabled: boolean; }) { const t = useTranslations(); const api = createApiClient(useEnvContext()); @@ -220,31 +233,36 @@ function SshServerForm({ {t("rdpServerDescription")} - - - - - -
- -
+
+ + + + + +
+ +
+
); } diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx index de207b872..1c65eef91 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx @@ -11,10 +11,13 @@ import { } from "@app/components/Settings"; import { StrategySelect, StrategyOption } from "@app/components/StrategySelect"; import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { SitesSelector, type Selectedsite } from "@app/components/site-selector"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import { Button } from "@app/components/ui/button"; import { Input } from "@app/components/ui/input"; import { @@ -68,13 +71,21 @@ export default function SshSettingsPage(props: { }) { const params = use(props.params); const { resource, updateResource } = useResourceContext(); + const { isPaidUser } = usePaidStatus(); + const disabled = !isPaidUser( + tierMatrix[TierFeature.AdvancedPublicResources] + ); return ( + ); @@ -83,11 +94,13 @@ export default function SshSettingsPage(props: { function SshServerForm({ orgId, resource, - updateResource + updateResource, + disabled }: { orgId: string; resource: GetResourceResponse; updateResource: ResourceContextType["updateResource"]; + disabled: boolean; }) { const t = useTranslations(); const api = createApiClient(useEnvContext()); @@ -366,6 +379,10 @@ function SshServerForm({ {t("sshServerDescription")} +
@@ -520,6 +537,7 @@ function SshServerForm({ {t("saveSettings")} +
); } diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx index 93c35925e..51efd0311 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx @@ -10,11 +10,14 @@ import { SettingsSectionTitle } from "@app/components/Settings"; import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { type Selectedsite } from "@app/components/site-selector"; import { Button } from "@app/components/ui/button"; import { toast } from "@app/hooks/useToast"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import { createApiClient } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api/formatAxiosError"; import { useQuery } from "@tanstack/react-query"; @@ -46,13 +49,21 @@ export default function SshSettingsPage(props: { }) { const params = use(props.params); const { resource, updateResource } = useResourceContext(); + const { isPaidUser } = usePaidStatus(); + const disabled = !isPaidUser( + tierMatrix[TierFeature.AdvancedPublicResources] + ); return ( + ); @@ -61,11 +72,13 @@ export default function SshSettingsPage(props: { function SshServerForm({ orgId, resource, - updateResource + updateResource, + disabled }: { orgId: string; resource: GetResourceResponse; updateResource: ResourceContextType["updateResource"]; + disabled: boolean; }) { const t = useTranslations(); const api = createApiClient(useEnvContext()); @@ -218,31 +231,36 @@ function SshServerForm({ {t("vncServerDescription")} - - - - - -
- -
+
+ + + + + +
+ +
+
); } diff --git a/src/app/[orgId]/settings/resources/public/create/page.tsx b/src/app/[orgId]/settings/resources/public/create/page.tsx index 93a2d6d72..93c8e8934 100644 --- a/src/app/[orgId]/settings/resources/public/create/page.tsx +++ b/src/app/[orgId]/settings/resources/public/create/page.tsx @@ -72,7 +72,10 @@ import { } from "@app/components/ui/tooltip"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { DockerManager, DockerState } from "@app/lib/docker"; import { orgQueries } from "@app/lib/queries"; @@ -226,6 +229,8 @@ export default function Page() { orgQueries.sites({ orgId: orgId as string }) ); + const { isPaidUser } = usePaidStatus(); + const [remoteExitNodes, setRemoteExitNodes] = useState< ListRemoteExitNodesResponse["remoteExitNodes"] >([]); @@ -238,6 +243,14 @@ export default function Page() { // Resource type state const [resourceType, setResourceType] = useState("http"); + const isBrowserGatewayType = + resourceType === "ssh" || + resourceType === "rdp" || + resourceType === "vnc"; + const browserGatewayDisabled = + isBrowserGatewayType && + !isPaidUser(tierMatrix[TierFeature.AdvancedPublicResources]); + // Target management state (managed by ProxyResourceTargetsForm; mirrored here for onSubmit) const [targets, setTargets] = useState([]); @@ -870,6 +883,14 @@ export default function Page() { {/* SSH Server Section */} {resourceType === "ssh" && ( + {t("sshServer")} @@ -878,6 +899,14 @@ export default function Page() { {t("sshServerDescription")} +
{/* Mode */} @@ -1098,12 +1127,21 @@ export default function Page() { +
)} {/* RDP Server Section */} {resourceType === "rdp" && ( + {t("rdpServer")} @@ -1112,6 +1150,14 @@ export default function Page() { {t("rdpServerDescription")} +
+
)} {/* VNC Server Section */} {resourceType === "vnc" && ( + {t("vncServer")} @@ -1150,6 +1205,14 @@ export default function Page() { {t("vncServerDescription")} +
+
)} @@ -1225,7 +1289,7 @@ export default function Page() { } }} loading={createLoading} - disabled={!areAllTargetsValid()} + disabled={!areAllTargetsValid() || browserGatewayDisabled} > {t("resourceCreate")} diff --git a/src/components/CreateRoleForm.tsx b/src/components/CreateRoleForm.tsx index 537618ec0..080d5d293 100644 --- a/src/components/CreateRoleForm.tsx +++ b/src/components/CreateRoleForm.tsx @@ -16,10 +16,7 @@ import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; -import type { - CreateRoleBody, - CreateRoleResponse -} from "@server/routers/role"; +import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role"; import { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; import { useTransition } from "react"; @@ -50,7 +47,7 @@ export default function CreateRoleForm({ requireDeviceApproval: values.requireDeviceApproval, allowSsh: values.allowSsh }; - if (isPaidUser(tierMatrix.sshPam)) { + if (isPaidUser(tierMatrix.advancedPrivateResources)) { payload.sshSudoMode = values.sshSudoMode; payload.sshCreateHomeDir = values.sshCreateHomeDir; payload.sshSudoCommands = @@ -69,10 +66,9 @@ export default function CreateRoleForm({ } } const res = await api - .put>( - `/org/${org?.org.orgId}/role`, - payload - ) + .put< + AxiosResponse + >(`/org/${org?.org.orgId}/role`, payload) .catch((e) => { toast({ variant: "destructive", diff --git a/src/components/EditRoleForm.tsx b/src/components/EditRoleForm.tsx index aea0b93ad..8fa674bd1 100644 --- a/src/components/EditRoleForm.tsx +++ b/src/components/EditRoleForm.tsx @@ -16,10 +16,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import type { Role } from "@server/db"; -import type { - UpdateRoleBody, - UpdateRoleResponse -} from "@server/routers/role"; +import type { UpdateRoleBody, UpdateRoleResponse } from "@server/routers/role"; import { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; import { useTransition } from "react"; @@ -53,7 +50,7 @@ export default function EditRoleForm({ payload.name = values.name; payload.description = values.description || undefined; } - if (isPaidUser(tierMatrix.sshPam)) { + if (isPaidUser(tierMatrix.advancedPrivateResources)) { payload.sshSudoMode = values.sshSudoMode; payload.sshCreateHomeDir = values.sshCreateHomeDir; payload.sshSudoCommands = @@ -72,10 +69,9 @@ export default function EditRoleForm({ } } const res = await api - .post>( - `/role/${role.roleId}`, - payload - ) + .post< + AxiosResponse + >(`/role/${role.roleId}`, payload) .catch((e) => { toast({ variant: "destructive", diff --git a/src/components/PrivateResourceForm.tsx b/src/components/PrivateResourceForm.tsx index fb80e0ce0..856e18885 100644 --- a/src/components/PrivateResourceForm.tsx +++ b/src/components/PrivateResourceForm.tsx @@ -224,8 +224,10 @@ export function PrivateResourceForm({ const { env } = useEnvContext(); const { isPaidUser } = usePaidStatus(); const disableEnterpriseFeatures = env.flags.disableEnterpriseFeatures; - const sshSectionDisabled = !isPaidUser(tierMatrix.sshPam); - const httpSectionDisabled = !isPaidUser(tierMatrix.httpPrivateResources); + const sshSectionDisabled = !isPaidUser(tierMatrix.advancedPrivateResources); + const httpSectionDisabled = !isPaidUser( + tierMatrix.advancedPrivateResources + ); const nameRequiredKey = variant === "create" @@ -594,6 +596,7 @@ export function PrivateResourceForm({ const httpConfigDomainId = form.watch("httpConfigDomainId"); const httpConfigFullDomain = form.watch("httpConfigFullDomain"); const isHttpMode = mode === "http"; + const isSshMode = mode === "ssh"; const authDaemonMode = form.watch("authDaemonMode") ?? "site"; const pamMode = form.watch("pamMode") ?? "passthrough"; const isNative = sshServerMode === "native"; @@ -739,8 +742,17 @@ export function PrivateResourceForm({ ]); useEffect(() => { - onSubmitDisabledChange?.(isHttpMode && httpSectionDisabled); - }, [isHttpMode, httpSectionDisabled, onSubmitDisabledChange]); + onSubmitDisabledChange?.( + (isHttpMode && httpSectionDisabled) || + (isSshMode && sshSectionDisabled) + ); + }, [ + isHttpMode, + httpSectionDisabled, + isSshMode, + sshSectionDisabled, + onSubmitDisabledChange + ]); return (
@@ -1129,8 +1141,10 @@ export function PrivateResourceForm({ "" } disabled={ - isHttpMode && - httpSectionDisabled + (isHttpMode && + httpSectionDisabled) || + (isSshMode && + sshSectionDisabled) } onChange={(e) => field.onChange( @@ -1169,6 +1183,10 @@ export function PrivateResourceForm({ field.value ?? "" } + disabled={ + isSshMode && + sshSectionDisabled + } /> @@ -1202,7 +1220,10 @@ export function PrivateResourceForm({ "" } disabled={ - httpSectionDisabled + (isHttpMode && + httpSectionDisabled) || + (isSshMode && + sshSectionDisabled) } onChange={(e) => { const raw = @@ -1237,9 +1258,9 @@ export function PrivateResourceForm({ - {isHttpMode && ( + {(isHttpMode || isSshMode) && ( )} @@ -1773,7 +1794,9 @@ export function PrivateResourceForm({ {/* SSH Access tab (ssh mode only) */} {!disableEnterpriseFeatures && mode === "ssh" && (
- + {/* Mode */}
diff --git a/src/components/RoleForm.tsx b/src/components/RoleForm.tsx index 2194344b7..9b8d7034b 100644 --- a/src/components/RoleForm.tsx +++ b/src/components/RoleForm.tsx @@ -164,7 +164,7 @@ export function RoleForm({ } }, [variant, role, form]); - const sshDisabled = !isPaidUser(tierMatrix.sshPam); + const sshDisabled = !isPaidUser(tierMatrix.advancedPrivateResources); const sshSudoMode = form.watch("sshSudoMode"); const isAdminRole = variant === "edit" && role?.isAdmin === true; @@ -319,7 +319,9 @@ export function RoleForm({ {/* SSH tab - hidden when enterprise features are disabled */} {!env.flags.disableEnterpriseFeatures && (
- +