Remove advanced resources paywall

This commit is contained in:
Owen
2026-08-14 16:33:05 -04:00
parent ca7ea72ff1
commit b51aecf45a
30 changed files with 368 additions and 715 deletions
+4 -6
View File
@@ -10,7 +10,7 @@ export enum TierFeature {
ActionLogs = "actionLogs", // set the retention period to none on downgrade ActionLogs = "actionLogs", // set the retention period to none on downgrade
ConnectionLogs = "connectionLogs", ConnectionLogs = "connectionLogs",
RotateCredentials = "rotateCredentials", RotateCredentials = "rotateCredentials",
MaintencePage = "maintencePage", // handle downgrade MaintenancePage = "maintenancePage", // handle downgrade
DevicePosture = "devicePosture", DevicePosture = "devicePosture",
TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
@@ -25,8 +25,7 @@ export enum TierFeature {
WildcardSubdomain = "wildcardSubdomain", WildcardSubdomain = "wildcardSubdomain",
NewtAutoUpdate = "newtAutoUpdate", NewtAutoUpdate = "newtAutoUpdate",
ResourcePolicies = "resourcePolicies", ResourcePolicies = "resourcePolicies",
AdvancedPublicResources = "advancedPublicResources", RoleBasedSSHControls = "roleBasedSSHControls"
AdvancedPrivateResources = "advancedPrivateResources"
} }
export const tierMatrix: Record<TierFeature, Tier[]> = { export const tierMatrix: Record<TierFeature, Tier[]> = {
@@ -39,7 +38,7 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"], [TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"], [TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.MaintenancePage]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"], [TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
[TierFeature.TwoFactorEnforcement]: [ [TierFeature.TwoFactorEnforcement]: [
"tier1", "tier1",
@@ -69,6 +68,5 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"], [TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
[TierFeature.AdvancedPublicResources]: ["tier3", "enterprise"], [TierFeature.RoleBasedSSHControls]: ["tier3", "enterprise"]
[TierFeature.AdvancedPrivateResources]: ["tier3", "enterprise"]
}; };
-24
View File
@@ -128,30 +128,6 @@ export async function updatePrivateResources(
for (const [resourceNiceId, resourceData] of Object.entries( for (const [resourceNiceId, resourceData] of Object.entries(
config["client-resources"] config["client-resources"]
)) { )) {
if (resourceData.mode === "http") {
const hasHttpFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix.advancedPrivateResources
);
if (!hasHttpFeature) {
throw new Error(
"HTTP private resources are not included in your current plan. Please upgrade."
);
}
}
if (resourceData.mode === "ssh") {
const hasSshFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix.advancedPrivateResources
);
if (!hasSshFeature) {
throw new Error(
"SSH private resources are not included in your current plan. Please upgrade."
);
}
}
const [existingResource] = await trx const [existingResource] = await trx
.select() .select()
.from(siteResources) .from(siteResources)
+2 -14
View File
@@ -262,18 +262,6 @@ export async function updatePublicResources(
headers = JSON.stringify(resourceData.headers); headers = JSON.stringify(resourceData.headers);
} }
if (["ssh", "rdp", "vnc"].includes(resourceData.mode || "")) {
const isLicensed = await isLicensedOrSubscribed(
orgId,
tierMatrix.advancedPublicResources
);
if (!isLicensed) {
throw new Error(
"Your current subscription does not support browser gateway resources. Please upgrade to access this feature."
);
}
}
if (resourceData.policy) { if (resourceData.policy) {
const isLicensed = await isLicensedOrSubscribed( const isLicensed = await isLicensedOrSubscribed(
orgId, orgId,
@@ -331,7 +319,7 @@ export async function updatePublicResources(
const isLicensed = await isLicensedOrSubscribed( const isLicensed = await isLicensedOrSubscribed(
orgId, orgId,
tierMatrix.maintencePage tierMatrix.maintenancePage
); );
if (!isLicensed) { if (!isLicensed) {
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
@@ -1138,7 +1126,7 @@ export async function updatePublicResources(
const isLicensed = await isLicensedOrSubscribed( const isLicensed = await isLicensedOrSubscribed(
orgId, orgId,
tierMatrix.maintencePage tierMatrix.maintenancePage
); );
if (!isLicensed) { if (!isLicensed) {
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
@@ -295,8 +295,8 @@ async function disableFeature(
await disableRotateCredentials(orgId); await disableRotateCredentials(orgId);
break; break;
case TierFeature.MaintencePage: case TierFeature.MaintenancePage:
await disableMaintencePage(orgId); await disablemaintenancePage(orgId);
break; break;
case TierFeature.DevicePosture: case TierFeature.DevicePosture:
@@ -319,10 +319,6 @@ async function disableFeature(
await disableAutoProvisioning(orgId); await disableAutoProvisioning(orgId);
break; break;
case TierFeature.AdvancedPrivateResources:
await disableAdvancedPrivateResources(orgId);
break;
case TierFeature.FullRbac: case TierFeature.FullRbac:
await disableFullRbac(orgId); await disableFullRbac(orgId);
break; break;
@@ -368,13 +364,6 @@ async function disableDeviceApprovals(orgId: string): Promise<void> {
logger.info(`Disabled device approvals on all roles for org ${orgId}`); logger.info(`Disabled device approvals on all roles for org ${orgId}`);
} }
async function disableAdvancedPrivateResources(orgId: string): Promise<void> {
// 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<void> { async function disableFullRbac(orgId: string): Promise<void> {
logger.info(`Disabled full RBAC for org ${orgId}`); logger.info(`Disabled full RBAC for org ${orgId}`);
} }
@@ -506,7 +495,7 @@ async function disableConnectionLogs(orgId: string): Promise<void> {
async function disableRotateCredentials(orgId: string): Promise<void> {} async function disableRotateCredentials(orgId: string): Promise<void> {}
async function disableMaintencePage(orgId: string): Promise<void> { async function disablemaintenancePage(orgId: string): Promise<void> {
await db await db
.update(resources) .update(resources)
.set({ .set({
+1 -27
View File
@@ -20,19 +20,16 @@ import * as orgIdp from "#private/routers/orgIdp";
import * as domain from "#private/routers/domain"; import * as domain from "#private/routers/domain";
import * as auth from "#private/routers/auth"; import * as auth from "#private/routers/auth";
import * as license from "#private/routers/license"; import * as license from "#private/routers/license";
import * as generateLicense from "./generatedLicense"; import * as generateLicense from "#private/routers/generatedLicense";
import * as logs from "#private/routers/auditLogs"; import * as logs from "#private/routers/auditLogs";
import * as misc from "#private/routers/misc"; import * as misc from "#private/routers/misc";
import * as reKey from "#private/routers/re-key"; import * as reKey from "#private/routers/re-key";
import * as approval from "#private/routers/approvals"; import * as approval from "#private/routers/approvals";
import * as ssh from "#private/routers/ssh";
import * as user from "#private/routers/user"; import * as user from "#private/routers/user";
import * as siteProvisioning from "#private/routers/siteProvisioning"; import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination"; import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule"; import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks"; import * as healthChecks from "#private/routers/healthChecks";
import * as client from "@server/routers/client";
import * as resource from "#private/routers/resource";
import * as policy from "#private/routers/policy"; import * as policy from "#private/routers/policy";
import { import {
@@ -652,17 +649,6 @@ authenticated.put(
reKey.reGenerateExitNodeSecret reKey.reGenerateExitNodeSecret
); );
authenticated.post(
"/org/:orgId/ssh/sign-key",
verifyValidLicense,
verifyValidSubscription(tierMatrix.advancedPrivateResources),
verifyOrgAccess,
verifyLimits,
// verifyUserHasAction(ActionsEnum.signSshKey), // this check happens inside of the function now
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
ssh.signSshKey
);
authenticated.post( authenticated.post(
"/user/:userId/add-role/:roleId", "/user/:userId/add-role/:roleId",
verifyRoleAccess, verifyRoleAccess,
@@ -868,18 +854,6 @@ authenticated.get(
healthChecks.getBatchedHealthCheckStatusHistory healthChecks.getBatchedHealthCheckStatusHistory
); );
authenticated.get(
"/client/:clientId/verify-associations-cache",
verifyClientAccess,
client.verifyClientAssociationsCache
);
authenticated.post(
"/client/:clientId/rebuild-associations-cache",
verifyClientAccess,
client.rebuildClientAssociationsCacheRoute
);
authenticated.post( authenticated.post(
"/org/:orgId/logs/access/attempt", "/org/:orgId/logs/access/attempt",
verifyOrgAccess, verifyOrgAccess,
-7
View File
@@ -17,7 +17,6 @@ import * as orgIdp from "#private/routers/orgIdp";
import * as billing from "#private/routers/billing"; import * as billing from "#private/routers/billing";
import * as license from "#private/routers/license"; import * as license from "#private/routers/license";
import * as resource from "#private/routers/resource"; import * as resource from "#private/routers/resource";
import * as ssh from "#private/routers/ssh";
import * as ws from "@server/routers/ws"; import * as ws from "@server/routers/ws";
import * as browserTarget from "#private/routers/browserGatewayTarget"; import * as browserTarget from "#private/routers/browserGatewayTarget";
@@ -47,12 +46,6 @@ internalRouter.get(`/license/status`, license.getLicenseStatus);
internalRouter.get("/maintenance/info", resource.getMaintenanceInfo); internalRouter.get("/maintenance/info", resource.getMaintenanceInfo);
internalRouter.post(
"/org/:orgId/ssh/sign-key",
verifyUserFromResourceSessionMiddleware,
ssh.signSshKey
);
internalRouter.get( internalRouter.get(
"/ws/round-trip-message/:messageId", "/ws/round-trip-message/:messageId",
verifyUserFromResourceSessionMiddleware, verifyUserFromResourceSessionMiddleware,
-14
View File
@@ -1,14 +0,0 @@
/*
* 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.
*/
export * from "./signSshKey";
+25 -1
View File
@@ -20,6 +20,7 @@ import * as logs from "./auditLogs";
import * as launcher from "./launcher"; import * as launcher from "./launcher";
import * as newt from "./newt"; import * as newt from "./newt";
import * as olm from "./olm"; import * as olm from "./olm";
import * as ssh from "./ssh";
import * as serverInfo from "./serverInfo"; import * as serverInfo from "./serverInfo";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { import {
@@ -56,12 +57,13 @@ import rateLimit, { ipKeyGenerator } from "express-rate-limit";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { build } from "@server/build"; import { build } from "@server/build";
import { createStore } from "#dynamic/lib/rateLimitStore"; import { createStore } from "#dynamic/lib/rateLimitStore";
import { logActionAudit } from "#dynamic/middlewares"; import { logActionAudit, verifyValidLicense } from "#dynamic/middlewares";
import { checkRoundTripMessage } from "./ws"; import { checkRoundTripMessage } from "./ws";
import * as labels from "@server/routers/labels"; import * as labels from "@server/routers/labels";
import * as aiProvider from "@server/routers/aiProvider"; import * as aiProvider from "@server/routers/aiProvider";
import * as aiBudget from "@server/routers/aiBudget"; import * as aiBudget from "@server/routers/aiBudget";
import * as virtualApiKey from "@server/routers/virtualApiKey"; import * as virtualApiKey from "@server/routers/virtualApiKey";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
// Root routes // Root routes
export const unauthenticated = Router(); export const unauthenticated = Router();
@@ -1863,6 +1865,28 @@ authenticated.put(
labels.detachLabelFromItem labels.detachLabelFromItem
); );
authenticated.post(
"/org/:orgId/ssh/sign-key",
verifyValidLicense,
verifyOrgAccess,
verifyLimits,
// verifyUserHasAction(ActionsEnum.signSshKey), // this check happens inside of the function now
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
ssh.signSshKey
);
authenticated.get(
"/client/:clientId/verify-associations-cache",
verifyClientAccess,
client.verifyClientAssociationsCache
);
authenticated.post(
"/client/:clientId/rebuild-associations-cache",
verifyClientAccess,
client.rebuildClientAssociationsCacheRoute
);
// Auth routes // Auth routes
export const authRouter = Router(); export const authRouter = Router();
unauthenticated.use("/auth", authRouter); unauthenticated.use("/auth", authRouter);
+11 -4
View File
@@ -1,15 +1,17 @@
import { Router } from "express"; import { Router } from "express";
import * as gerbil from "@server/routers/gerbil"; import * as gerbil from "@server/routers/gerbil";
import * as traefik from "@server/routers/traefik"; import * as traefik from "@server/routers/traefik";
import * as resource from "./resource"; import * as resource from "@server/routers/resource";
import * as badger from "./badger"; import * as badger from "@server/routers/badger";
import * as auth from "@server/routers/auth"; import * as auth from "@server/routers/auth";
import * as supporterKey from "@server/routers/supporterKey"; import * as supporterKey from "@server/routers/supporterKey";
import * as idp from "@server/routers/idp"; import * as idp from "@server/routers/idp";
import * as ssh from "@server/routers/ssh";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { import {
verifyResourceAccess, verifyResourceAccess,
verifySessionUserMiddleware verifySessionUserMiddleware,
verifyUserFromResourceSessionMiddleware
} from "@server/middlewares"; } from "@server/middlewares";
// Root routes // Root routes
@@ -42,6 +44,12 @@ internalRouter.get("/idp", idp.listIdps);
internalRouter.get("/idp/:idpId", idp.getIdp); internalRouter.get("/idp/:idpId", idp.getIdp);
internalRouter.post(
"/org/:orgId/ssh/sign-key",
verifyUserFromResourceSessionMiddleware,
ssh.signSshKey
);
// Gerbil routes // Gerbil routes
const gerbilRouter = Router(); const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter); internalRouter.use("/gerbil", gerbilRouter);
@@ -63,4 +71,3 @@ internalRouter.use("/badger", badgerRouter);
badgerRouter.post("/verify-session", badger.verifyResourceSession); badgerRouter.post("/verify-session", badger.verifyResourceSession);
badgerRouter.post("/exchange-session", badger.exchangeSession); badgerRouter.post("/exchange-session", badger.exchangeSession);
+1 -16
View File
@@ -31,7 +31,7 @@ import {
} from "@server/lib/domainUtils"; } from "@server/lib/domainUtils";
import { isSubscribed } from "#dynamic/lib/isSubscribed"; import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { import {
getUniqueResourceName, getUniqueResourceName,
getUniqueResourcePolicyName getUniqueResourcePolicyName
@@ -454,21 +454,6 @@ async function createHttpResource(
} }
} }
if (
["ssh", "rdp", "vnc"].includes(effectiveMode) &&
!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 // Validate domain and construct full domain
const domainResult = await validateAndConstructDomain( const domainResult = await validateAndConstructDomain(
domainId, domainId,
+1 -1
View File
@@ -135,7 +135,7 @@ export async function createRole(
const isLicensedSshPam = await isLicensedOrSubscribed( const isLicensedSshPam = await isLicensedOrSubscribed(
orgId, orgId,
tierMatrix.advancedPrivateResources tierMatrix.roleBasedSSHControls
); );
const roleInsertValues: Record<string, unknown> = { const roleInsertValues: Record<string, unknown> = {
name: roleData.name, name: roleData.name,
+1 -1
View File
@@ -144,7 +144,7 @@ export async function updateRole(
const isLicensedSshPam = await isLicensedOrSubscribed( const isLicensedSshPam = await isLicensedOrSubscribed(
orgId, orgId,
tierMatrix.advancedPrivateResources tierMatrix.roleBasedSSHControls
); );
if (!isLicensedSshPam) { if (!isLicensedSshPam) {
delete updateData.sshSudoMode; delete updateData.sshSudoMode;
@@ -10,8 +10,7 @@ import {
SiteResource, SiteResource,
siteResources, siteResources,
sites, sites,
userSiteResources, userSiteResources
primaryDb
} from "@server/db"; } from "@server/db";
import { getUniqueSiteResourceName } from "@server/db/names"; import { getUniqueSiteResourceName } from "@server/db/names";
import { import {
@@ -19,8 +18,6 @@ import {
isIpInCidr, isIpInCidr,
portRangeStringSchema portRangeStringSchema
} from "@server/lib/ip"; } from "@server/lib/ip";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { import {
rebuildClientAssociationsFromSiteResource, rebuildClientAssociationsFromSiteResource,
isOrgRebuildRateLimited isOrgRebuildRateLimited
@@ -408,21 +405,6 @@ export async function createSiteResource(
} }
} }
if (mode == "http") {
const hasHttpFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix[TierFeature.AdvancedPrivateResources]
);
if (!hasHttpFeature) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"HTTP private resources are not included in your current plan. Please upgrade."
)
);
}
}
// Verify the site exists and belongs to the org // Verify the site exists and belongs to the org
const sitesToAssign = await db const sitesToAssign = await db
.select() .select()
@@ -557,20 +539,6 @@ export async function createSiteResource(
} }
} }
const isLicensedSshPam = await isLicensedOrSubscribed(
orgId,
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; let updatedNiceId = niceId;
if (!niceId) { if (!niceId) {
updatedNiceId = await getUniqueSiteResourceName(orgId); updatedNiceId = await getUniqueSiteResourceName(orgId);
@@ -646,13 +614,13 @@ export async function createSiteResource(
fullDomain, fullDomain,
requiresExitNodeConnection: mode === "inference" // in the future we might want to have different modes that do this requiresExitNodeConnection: mode === "inference" // in the future we might want to have different modes that do this
}; };
if (isLicensedSshPam) {
if (authDaemonPort !== undefined) if (authDaemonPort !== undefined)
insertValues.authDaemonPort = authDaemonPort; insertValues.authDaemonPort = authDaemonPort;
if (authDaemonMode !== undefined) if (authDaemonMode !== undefined)
insertValues.authDaemonMode = authDaemonMode; insertValues.authDaemonMode = authDaemonMode;
if (pamMode !== undefined) insertValues.pamMode = pamMode; if (pamMode !== undefined) insertValues.pamMode = pamMode;
}
[newSiteResource] = await trx [newSiteResource] = await trx
.insert(siteResources) .insert(siteResources)
.values(insertValues) .values(insertValues)
@@ -10,8 +10,6 @@ import {
sites, sites,
userSiteResources userSiteResources
} from "@server/db"; } from "@server/db";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateAndConstructDomain } from "@server/lib/domainUtils";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq, and, ne, inArray } from "drizzle-orm"; import { eq, and, ne, inArray } from "drizzle-orm";
@@ -362,26 +360,6 @@ export async function updateSiteResource(
); );
} }
if (mode == "http") {
const hasHttpFeature = await isLicensedOrSubscribed(
existingSiteResource.orgId,
tierMatrix[TierFeature.AdvancedPrivateResources]
);
if (!hasHttpFeature) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"HTTP private resources are not included in your current plan. Please upgrade."
)
);
}
}
const isLicensedSshPam = await isLicensedOrSubscribed(
existingSiteResource.orgId,
tierMatrix.advancedPrivateResources
);
const [org] = await db const [org] = await db
.select() .select()
.from(orgs) .from(orgs)
@@ -541,10 +519,9 @@ export async function updateSiteResource(
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// Update the site resource // Update the site resource
const sshPamSet = const sshPamSet =
isLicensedSshPam && authDaemonPort !== undefined ||
(authDaemonPort !== undefined ||
authDaemonMode !== undefined || authDaemonMode !== undefined ||
pamMode !== undefined) pamMode !== undefined
? { ? {
...(authDaemonPort !== undefined && { ...(authDaemonPort !== undefined && {
authDaemonPort authDaemonPort
+1
View File
@@ -0,0 +1 @@
export * from "./signSshKey";
@@ -1,16 +1,3 @@
/*
* 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 { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { randomInt } from "crypto"; import { randomInt } from "crypto";
import { z } from "zod"; import { z } from "zod";
@@ -35,8 +22,6 @@ import {
SiteResource SiteResource
} from "@server/db"; } from "@server/db";
import { logAccessAudit } from "#private/lib/logAccessAudit"; import { logAccessAudit } from "#private/lib/logAccessAudit";
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -163,19 +148,6 @@ export async function signSshKey(
); );
} }
const isLicensed = await isLicensedOrSubscribed(
orgId,
tierMatrix.advancedPrivateResources
);
if (!isLicensed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"SSH key signing requires a paid plan"
)
);
}
// Get and decrypt the org's CA keys // Get and decrypt the org's CA keys
const caKeys = await getOrgCAKeys( const caKeys = await getOrgCAKeys(
orgId, orgId,
@@ -35,10 +35,6 @@ import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
export default function PrivateResourceHttpPage() { export default function PrivateResourceHttpPage() {
const t = useTranslations(); const t = useTranslations();
const { save, siteResource } = useSaveSiteResource(); const { save, siteResource } = useSaveSiteResource();
const { isPaidUser } = usePaidStatus();
const httpSectionDisabled = !isPaidUser(
tierMatrix.advancedPrivateResources
);
const [selectedSites, setSelectedSites] = useState(() => const [selectedSites, setSelectedSites] = useState(() =>
buildSelectedSitesForResource(siteResource) buildSelectedSitesForResource(siteResource)
); );
@@ -120,7 +116,7 @@ export default function PrivateResourceHttpPage() {
)} )}
orgId={siteResource.orgId} orgId={siteResource.orgId}
watch={asAnyWatch(form.watch)} watch={asAnyWatch(form.watch)}
disabled={httpSectionDisabled} disabled={false}
siteResourceId={siteResource.id} siteResourceId={siteResource.id}
/> />
</SettingsFormCell> </SettingsFormCell>
@@ -135,7 +131,6 @@ export default function PrivateResourceHttpPage() {
type="submit" type="submit"
form="private-resource-http-form" form="private-resource-http-form"
loading={saveLoading} loading={saveLoading}
disabled={httpSectionDisabled}
> >
{t("saveSettings")} {t("saveSettings")}
</Button> </Button>
@@ -12,16 +12,13 @@ import {
SettingsFormGrid SettingsFormGrid
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SshServerSettingsFields } from "@app/components/SshServerSettingsFields"; import { SshServerSettingsFields } from "@app/components/SshServerSettingsFields";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { Form } from "@app/components/ui/form"; import { Form } from "@app/components/ui/form";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { import {
createSshFormSchema, createSshFormSchema,
inferSshPamMode inferSshPamMode
} from "@app/lib/privateResourceForm"; } from "@app/lib/privateResourceForm";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react"; import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -39,8 +36,6 @@ import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
export default function PrivateResourceSshPage() { export default function PrivateResourceSshPage() {
const t = useTranslations(); const t = useTranslations();
const { save, siteResource } = useSaveSiteResource(); const { save, siteResource } = useSaveSiteResource();
const { isPaidUser } = usePaidStatus();
const sshSectionDisabled = !isPaidUser(tierMatrix.advancedPrivateResources);
const isNative = siteResource.authDaemonMode === "native"; const isNative = siteResource.authDaemonMode === "native";
const [sshServerMode] = useState<"standard" | "native">( const [sshServerMode] = useState<"standard" | "native">(
isNative ? "native" : "standard" isNative ? "native" : "standard"
@@ -150,7 +145,6 @@ export default function PrivateResourceSshPage() {
return ( return (
<SettingsContainer> <SettingsContainer>
<PaidFeaturesAlert tiers={tierMatrix.advancedPrivateResources} />
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -161,14 +155,6 @@ export default function PrivateResourceSshPage() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset
disabled={sshSectionDisabled}
className={
sshSectionDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<Form {...form}> <Form {...form}>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
@@ -185,11 +171,9 @@ export default function PrivateResourceSshPage() {
handleDaemonLocationChange handleDaemonLocationChange
} }
onAuthDaemonPortChange={(value) => onAuthDaemonPortChange={(value) =>
form.setValue( form.setValue("authDaemonPort", value, {
"authDaemonPort", shouldValidate: true
value, })
{ shouldValidate: true }
)
} }
authDaemonPortError={ authDaemonPortError={
form.formState.errors.authDaemonPort form.formState.errors.authDaemonPort
@@ -207,7 +191,6 @@ export default function PrivateResourceSshPage() {
onSelectedSitesChange={setSelectedSites} onSelectedSitesChange={setSelectedSites}
showSshSettings={false} showSshSettings={false}
embedInParentGrid embedInParentGrid
showPaidFeaturesAlert={false}
isNativeSsh={isNative} isNativeSsh={isNative}
/> />
</SettingsFormGrid> </SettingsFormGrid>
@@ -222,7 +205,6 @@ export default function PrivateResourceSshPage() {
</form> </form>
</SettingsSectionFooter> </SettingsSectionFooter>
</Form> </Form>
</fieldset>
</SettingsSection> </SettingsSection>
</SettingsContainer> </SettingsContainer>
); );
@@ -16,7 +16,6 @@ import {
type DescribedSelectOption type DescribedSelectOption
} from "@app/components/DescribedSelect"; } from "@app/components/DescribedSelect";
import DomainPicker from "@app/components/DomainPicker"; import DomainPicker from "@app/components/DomainPicker";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@@ -30,7 +29,6 @@ import {
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import type { Selectedsite } from "@app/components/site-selector"; import type { Selectedsite } from "@app/components/site-selector";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { import {
@@ -77,12 +75,6 @@ export default function CreatePrivateResourcePage() {
const { env } = useEnvContext(); const { env } = useEnvContext();
const api = createApiClient({ env }); const api = createApiClient({ env });
const orgId = params.orgId as string; const orgId = params.orgId as string;
const disableEnterpriseFeatures = env.flags.disableEnterpriseFeatures;
const { isPaidUser } = usePaidStatus();
const httpSectionDisabled = !isPaidUser(
tierMatrix.advancedPrivateResources
);
const sshSectionDisabled = !isPaidUser(tierMatrix.advancedPrivateResources);
const [isSubmitting, startTransition] = useTransition(); const [isSubmitting, startTransition] = useTransition();
const siteIdParam = searchParams.get("siteId"); const siteIdParam = searchParams.get("siteId");
@@ -158,8 +150,6 @@ export default function CreatePrivateResourcePage() {
title: t("createInternalResourceDialogModeCidr"), title: t("createInternalResourceDialogModeCidr"),
description: t("privateResourceTypeCidrDescription") description: t("privateResourceTypeCidrDescription")
}, },
...(!disableEnterpriseFeatures
? [
{ {
value: "http" as const, value: "http" as const,
title: t("createInternalResourceDialogModeHttp"), title: t("createInternalResourceDialogModeHttp"),
@@ -169,9 +159,7 @@ export default function CreatePrivateResourcePage() {
value: "ssh" as const, value: "ssh" as const,
title: t("createInternalResourceDialogModeSsh"), title: t("createInternalResourceDialogModeSsh"),
description: t("privateResourceTypeSshDescription") description: t("privateResourceTypeSshDescription")
} },
]
: []),
{ {
value: "inference" as const, value: "inference" as const,
title: t("createInternalResourceDialogModeInference"), title: t("createInternalResourceDialogModeInference"),
@@ -179,11 +167,6 @@ export default function CreatePrivateResourcePage() {
} }
]; ];
const submitDisabled =
isSubmitting ||
(mode === "http" && httpSectionDisabled) ||
(mode === "ssh" && sshSectionDisabled);
function onSubmit(values: FormValues) { function onSubmit(values: FormValues) {
startTransition(async () => { startTransition(async () => {
try { try {
@@ -467,10 +450,7 @@ export default function CreatePrivateResourcePage() {
)} )}
watch={asAnyWatch(form.watch)} watch={asAnyWatch(form.watch)}
labelPrefix="create" labelPrefix="create"
disabled={ disabled={false}
mode === "ssh" &&
sshSectionDisabled
}
/> />
</SettingsFormCell> </SettingsFormCell>
)} )}
@@ -584,9 +564,6 @@ export default function CreatePrivateResourcePage() {
{/* HTTP configuration */} {/* HTTP configuration */}
{mode === "http" && ( {mode === "http" && (
<SettingsSection> <SettingsSection>
<PaidFeaturesAlert
tiers={tierMatrix.advancedPrivateResources}
/>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("httpSettings")} {t("httpSettings")}
@@ -597,14 +574,7 @@ export default function CreatePrivateResourcePage() {
)} )}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset
disabled={httpSectionDisabled}
className={
httpSectionDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<SettingsFormGrid> <SettingsFormGrid>
@@ -612,9 +582,7 @@ export default function CreatePrivateResourcePage() {
<PrivateResourceSitesField <PrivateResourceSitesField
control={form.control} control={form.control}
orgId={orgId} orgId={orgId}
selectedSites={ selectedSites={selectedSites}
selectedSites
}
onSelectedSitesChange={ onSelectedSitesChange={
setSelectedSites setSelectedSites
} }
@@ -629,30 +597,21 @@ export default function CreatePrivateResourcePage() {
form.setValue form.setValue
)} )}
orgId={orgId} orgId={orgId}
watch={asAnyWatch( watch={asAnyWatch(form.watch)}
form.watch disabled={true}
)}
disabled={
httpSectionDisabled
}
labelPrefix="create" labelPrefix="create"
hideDomainPicker hideDomainPicker
hidePaidFeaturesAlert
/> />
</SettingsFormCell> </SettingsFormCell>
</SettingsFormGrid> </SettingsFormGrid>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</fieldset>
</SettingsSection> </SettingsSection>
)} )}
{/* SSH server */} {/* SSH server */}
{mode === "ssh" && ( {mode === "ssh" && (
<SettingsSection> <SettingsSection>
<PaidFeaturesAlert
tiers={tierMatrix.advancedPrivateResources}
/>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("sshSettings")} {t("sshSettings")}
@@ -661,37 +620,23 @@ export default function CreatePrivateResourcePage() {
{t("sshServerDescription")} {t("sshServerDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset
disabled={sshSectionDisabled}
className={
sshSectionDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<PrivateResourceSshFields <PrivateResourceSshFields
control={asAnyControl(form.control)} control={asAnyControl(form.control)}
setValue={asAnySetValue( setValue={asAnySetValue(form.setValue)}
form.setValue
)}
watch={asAnyWatch(form.watch)} watch={asAnyWatch(form.watch)}
orgId={orgId} orgId={orgId}
disabled={sshSectionDisabled} disabled={false}
selectedSites={selectedSites} selectedSites={selectedSites}
onSelectedSitesChange={ onSelectedSitesChange={setSelectedSites}
setSelectedSites
}
labelPrefix="create" labelPrefix="create"
showSshSettings={true} showSshSettings={true}
layout="wizard" layout="wizard"
showPaidFeaturesAlert={false}
hideAlias hideAlias
/> />
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</fieldset>
</SettingsSection> </SettingsSection>
)} )}
@@ -776,7 +721,7 @@ export default function CreatePrivateResourcePage() {
<Button <Button
type="submit" type="submit"
form="create-private-resource-form" form="create-private-resource-form"
disabled={submitDisabled} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
> >
{t("createInternalResourceDialogCreateResource")} {t("createInternalResourceDialogCreateResource")}
@@ -161,7 +161,7 @@ export default function ResourceMaintenancePage() {
return null; return null;
} }
const isMaintenanceDisabled = !isPaidUser(tierMatrix.maintencePage); const isMaintenanceDisabled = !isPaidUser(tierMatrix.maintenancePage);
const maintenanceModeTypeOptions: StrategyOption< const maintenanceModeTypeOptions: StrategyOption<
"automatic" | "forced" "automatic" | "forced"
@@ -180,7 +180,7 @@ export default function ResourceMaintenancePage() {
return ( return (
<> <>
<PaidFeaturesAlert tiers={tierMatrix.maintencePage} /> <PaidFeaturesAlert tiers={tierMatrix.maintenancePage} />
<div <div
className={ className={
isMaintenanceDisabled isMaintenanceDisabled
@@ -55,11 +55,7 @@ export default function RdpSettingsPage(props: {
}) { }) {
const params = use(props.params); const params = use(props.params);
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const { isPaidUser } = usePaidStatus();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const disabled = !isPaidUser(
tierMatrix[TierFeature.AdvancedPublicResources]
);
const { data: targetsResponse, isLoading: isLoadingTargets } = useQuery({ const { data: targetsResponse, isLoading: isLoadingTargets } = useQuery({
queryKey: ["resourceTargets", resource.resourceId, params.orgId, "rdp"], queryKey: ["resourceTargets", resource.resourceId, params.orgId, "rdp"],
@@ -75,14 +71,11 @@ export default function RdpSettingsPage(props: {
return ( return (
<SettingsContainer> <SettingsContainer>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
/>
<RdpServerForm <RdpServerForm
orgId={params.orgId} orgId={params.orgId}
resource={resource} resource={resource}
updateResource={updateResource} updateResource={updateResource}
disabled={disabled} disabled={false}
targetsResponse={targetsResponse ?? { targets: [] }} targetsResponse={targetsResponse ?? { targets: [] }}
/> />
</SettingsContainer> </SettingsContainer>
@@ -75,11 +75,7 @@ export default function SshSettingsPage(props: {
}) { }) {
const params = use(props.params); const params = use(props.params);
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const { isPaidUser } = usePaidStatus();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const disabled = !isPaidUser(
tierMatrix[TierFeature.AdvancedPublicResources]
);
const { data: targetsResponse, isLoading: isLoadingTargets } = useQuery({ const { data: targetsResponse, isLoading: isLoadingTargets } = useQuery({
queryKey: ["resourceTargets", resource.resourceId, params.orgId, "ssh"], queryKey: ["resourceTargets", resource.resourceId, params.orgId, "ssh"],
@@ -95,14 +91,11 @@ export default function SshSettingsPage(props: {
return ( return (
<SettingsContainer> <SettingsContainer>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
/>
<SshServerForm <SshServerForm
orgId={params.orgId} orgId={params.orgId}
resource={resource} resource={resource}
updateResource={updateResource} updateResource={updateResource}
disabled={disabled} disabled={false}
targetsResponse={targetsResponse ?? { targets: [] }} targetsResponse={targetsResponse ?? { targets: [] }}
/> />
</SettingsContainer> </SettingsContainer>
@@ -55,11 +55,7 @@ export default function VncSettingsPage(props: {
}) { }) {
const params = use(props.params); const params = use(props.params);
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const { isPaidUser } = usePaidStatus();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const disabled = !isPaidUser(
tierMatrix[TierFeature.AdvancedPublicResources]
);
const { data: targetsResponse, isLoading: isLoadingTargets } = useQuery({ const { data: targetsResponse, isLoading: isLoadingTargets } = useQuery({
queryKey: ["resourceTargets", resource.resourceId, params.orgId, "vnc"], queryKey: ["resourceTargets", resource.resourceId, params.orgId, "vnc"],
@@ -75,14 +71,11 @@ export default function VncSettingsPage(props: {
return ( return (
<SettingsContainer> <SettingsContainer>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
/>
<VncServerForm <VncServerForm
orgId={params.orgId} orgId={params.orgId}
resource={resource} resource={resource}
updateResource={updateResource} updateResource={updateResource}
disabled={disabled} disabled={true}
targetsResponse={targetsResponse ?? { targets: [] }} targetsResponse={targetsResponse ?? { targets: [] }}
/> />
</SettingsContainer> </SettingsContainer>
@@ -239,14 +239,6 @@ export default function Page() {
// Resource type state // Resource type state
const [resourceType, setResourceType] = useState<NewResourceType>("http"); const [resourceType, setResourceType] = useState<NewResourceType>("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) // Target management state (managed by ProxyResourceTargetsForm; mirrored here for onSubmit)
const [targets, setTargets] = useState<LocalTarget[]>([]); const [targets, setTargets] = useState<LocalTarget[]>([]);
const [selectedProviders, setSelectedProviders] = useState< const [selectedProviders, setSelectedProviders] = useState<
@@ -1056,14 +1048,6 @@ export default function Page() {
{/* SSH Server Section */} {/* SSH Server Section */}
{resourceType === "ssh" && ( {resourceType === "ssh" && (
<SettingsSection> <SettingsSection>
<PaidFeaturesAlert
tiers={
tierMatrix[
TierFeature
.AdvancedPublicResources
]
}
/>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("sshServer")} {t("sshServer")}
@@ -1072,14 +1056,7 @@ export default function Page() {
{t("sshServerDescription")} {t("sshServerDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset
disabled={browserGatewayDisabled}
className={
browserGatewayDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<SettingsFormGrid> <SettingsFormGrid>
@@ -1318,21 +1295,12 @@ export default function Page() {
</SettingsFormGrid> </SettingsFormGrid>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</fieldset>
</SettingsSection> </SettingsSection>
)} )}
{/* RDP Server Section */} {/* RDP Server Section */}
{resourceType === "rdp" && ( {resourceType === "rdp" && (
<SettingsSection> <SettingsSection>
<PaidFeaturesAlert
tiers={
tierMatrix[
TierFeature
.AdvancedPublicResources
]
}
/>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("rdpServer")} {t("rdpServer")}
@@ -1341,14 +1309,6 @@ export default function Page() {
{t("rdpServerDescription")} {t("rdpServerDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset
disabled={browserGatewayDisabled}
className={
browserGatewayDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...bgTargetForm}> <Form {...bgTargetForm}>
@@ -1365,21 +1325,12 @@ export default function Page() {
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</fieldset>
</SettingsSection> </SettingsSection>
)} )}
{/* VNC Server Section */} {/* VNC Server Section */}
{resourceType === "vnc" && ( {resourceType === "vnc" && (
<SettingsSection> <SettingsSection>
<PaidFeaturesAlert
tiers={
tierMatrix[
TierFeature
.AdvancedPublicResources
]
}
/>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("vncServer")} {t("vncServer")}
@@ -1388,14 +1339,7 @@ export default function Page() {
{t("vncServerDescription")} {t("vncServerDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset
disabled={browserGatewayDisabled}
className={
browserGatewayDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...bgTargetForm}> <Form {...bgTargetForm}>
@@ -1412,7 +1356,6 @@ export default function Page() {
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</fieldset>
</SettingsSection> </SettingsSection>
)} )}
@@ -1527,7 +1470,6 @@ export default function Page() {
loading={createLoading} loading={createLoading}
disabled={ disabled={
!areAllTargetsValid() || !areAllTargetsValid() ||
browserGatewayDisabled ||
createLoading createLoading
} }
> >
+1 -1
View File
@@ -52,7 +52,7 @@ export default function CreateRoleForm({
requireDeviceApproval: values.requireDeviceApproval, requireDeviceApproval: values.requireDeviceApproval,
allowSsh: values.allowSsh allowSsh: values.allowSsh
}; };
if (isPaidUser(tierMatrix.advancedPrivateResources)) { if (isPaidUser(tierMatrix.roleBasedSSHControls)) {
payload.sshSudoMode = values.sshSudoMode; payload.sshSudoMode = values.sshSudoMode;
payload.sshCreateHomeDir = values.sshCreateHomeDir; payload.sshCreateHomeDir = values.sshCreateHomeDir;
payload.sshSudoCommands = payload.sshSudoCommands =
+2 -5
View File
@@ -59,7 +59,7 @@ export default function EditRoleForm({
payload.name = values.name; payload.name = values.name;
payload.description = values.description || undefined; payload.description = values.description || undefined;
} }
if (isPaidUser(tierMatrix.advancedPrivateResources)) { if (isPaidUser(tierMatrix.roleBasedSSHControls)) {
payload.sshSudoMode = values.sshSudoMode; payload.sshSudoMode = values.sshSudoMode;
payload.sshCreateHomeDir = values.sshCreateHomeDir; payload.sshCreateHomeDir = values.sshCreateHomeDir;
payload.sshSudoCommands = payload.sshSudoCommands =
@@ -107,10 +107,7 @@ export default function EditRoleForm({
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("aiBudgetErrorSave"), title: t("aiBudgetErrorSave"),
description: formatAxiosError( description: formatAxiosError(e, t("aiBudgetErrorSave"))
e,
t("aiBudgetErrorSave")
)
}); });
} }
} }
+1 -12
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import DomainPicker from "@app/components/DomainPicker"; import DomainPicker from "@app/components/DomainPicker";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { import {
SettingsFormCell, SettingsFormCell,
SettingsFormGrid, SettingsFormGrid,
@@ -25,7 +24,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import type { Control, UseFormSetValue, UseFormWatch } from "react-hook-form"; import type { Control, UseFormSetValue, UseFormWatch } from "react-hook-form";
@@ -49,8 +47,7 @@ export function PrivateResourceHttpFields({
disabled = false, disabled = false,
siteResourceId, siteResourceId,
labelPrefix = "edit", labelPrefix = "edit",
hideDomainPicker = false, hideDomainPicker = false
hidePaidFeaturesAlert = false
}: PrivateResourceHttpFieldsProps) { }: PrivateResourceHttpFieldsProps) {
const t = useTranslations(); const t = useTranslations();
const schemeLabelKey = const schemeLabelKey =
@@ -88,14 +85,6 @@ export function PrivateResourceHttpFields({
return ( return (
<SettingsFormGrid> <SettingsFormGrid>
{!hidePaidFeaturesAlert && (
<SettingsFormCell span="full">
<PaidFeaturesAlert
tiers={tierMatrix.advancedPrivateResources}
/>
</SettingsFormCell>
)}
<SettingsFormCell span="quarter"> <SettingsFormCell span="quarter">
<FormField <FormField
control={control} control={control}
@@ -38,7 +38,6 @@ type PrivateResourceSshFieldsProps = {
labelPrefix?: "create" | "edit"; labelPrefix?: "create" | "edit";
showSshSettings?: boolean; showSshSettings?: boolean;
layout?: "default" | "wizard"; layout?: "default" | "wizard";
showPaidFeaturesAlert?: boolean;
hideAlias?: boolean; hideAlias?: boolean;
embedInParentGrid?: boolean; embedInParentGrid?: boolean;
isNativeSsh?: boolean; isNativeSsh?: boolean;
@@ -55,7 +54,6 @@ export function PrivateResourceSshFields({
labelPrefix = "edit", labelPrefix = "edit",
showSshSettings = true, showSshSettings = true,
layout = "default", layout = "default",
showPaidFeaturesAlert = true,
hideAlias = false, hideAlias = false,
embedInParentGrid = false, embedInParentGrid = false,
isNativeSsh: isNativeSshProp isNativeSsh: isNativeSshProp
@@ -313,13 +311,6 @@ export function PrivateResourceSshFields({
const content: ReactNode = ( const content: ReactNode = (
<> <>
{showPaidFeaturesAlert && layout === "default" && (
<SettingsFormCell span="full">
<PaidFeaturesAlert
tiers={tierMatrix.advancedPrivateResources}
/>
</SettingsFormCell>
)}
{sshSettingsFields} {sshSettingsFields}
{destinationSection} {destinationSection}
</> </>
+26 -24
View File
@@ -212,7 +212,7 @@ export function RoleForm({
} }
}, [variant, role, form]); }, [variant, role, form]);
const sshDisabled = !isPaidUser(tierMatrix.advancedPrivateResources); const sshDisabled = !isPaidUser(tierMatrix.roleBasedSSHControls);
const sshSudoMode = form.watch("sshSudoMode"); const sshSudoMode = form.watch("sshSudoMode");
const isAdminRole = variant === "edit" && role?.isAdmin === true; const isAdminRole = variant === "edit" && role?.isAdmin === true;
const [pendingImport, setPendingImport] = const [pendingImport, setPendingImport] =
@@ -235,12 +235,6 @@ export function RoleForm({
setAttemptedBudgetsSave(false); setAttemptedBudgetsSave(false);
}, [variant, budgetsQuery.data]); }, [variant, budgetsQuery.data]);
useEffect(() => {
if (sshDisabled) {
form.setValue("allowSsh", false);
}
}, [sshDisabled, form]);
async function handleFileDrop( async function handleFileDrop(
file: File, file: File,
field: RoleTextImportField field: RoleTextImportField
@@ -487,12 +481,7 @@ export function RoleForm({
/> />
</div> </div>
{/* SSH tab - hidden when enterprise features are disabled */}
{!env.flags.disableEnterpriseFeatures && (
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<PaidFeaturesAlert
tiers={tierMatrix.advancedPrivateResources}
/>
<FormField <FormField
control={form.control} control={form.control}
name="allowSsh" name="allowSsh"
@@ -514,9 +503,7 @@ export function RoleForm({
<FormLabel> <FormLabel>
{t("roleAllowSsh")} {t("roleAllowSsh")}
</FormLabel> </FormLabel>
<OptionSelect< <OptionSelect<"allow" | "disallow">
"allow" | "disallow"
>
options={allowSshOptions} options={allowSshOptions}
value={ value={
sshDisabled sshDisabled
@@ -535,15 +522,19 @@ export function RoleForm({
disabled={sshDisabled} disabled={sshDisabled}
/> />
<FormDescription> <FormDescription>
{t( {t("roleAllowSshDescription")}
"roleAllowSshDescription"
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
); );
}} }}
/> />
{/* SSH tab - hidden when enterprise features are disabled */}
{!env.flags.disableEnterpriseFeatures && (
<>
<PaidFeaturesAlert
tiers={tierMatrix.roleBasedSSHControls}
/>
<FormField <FormField
control={form.control} control={form.control}
name="sshSudoMode" name="sshSudoMode"
@@ -552,11 +543,15 @@ export function RoleForm({
[ [
{ {
value: "none", value: "none",
label: t("sshSudoModeNone") label: t(
"sshSudoModeNone"
)
}, },
{ {
value: "full", value: "full",
label: t("sshSudoModeFull") label: t(
"sshSudoModeFull"
)
}, },
{ {
value: "commands", value: "commands",
@@ -573,7 +568,9 @@ export function RoleForm({
<OptionSelect<SshSudoMode> <OptionSelect<SshSudoMode>
options={sudoOptions} options={sudoOptions}
value={field.value} value={field.value}
onChange={field.onChange} onChange={
field.onChange
}
cols={3} cols={3}
disabled={sshDisabled} disabled={sshDisabled}
/> />
@@ -604,7 +601,9 @@ export function RoleForm({
"roleTextFieldPlaceholder" "roleTextFieldPlaceholder"
) )
} }
disabled={sshDisabled} disabled={
sshDisabled
}
className={cn( className={cn(
"h-20 min-h-20", "h-20 min-h-20",
dragOverField === dragOverField ===
@@ -655,7 +654,9 @@ export function RoleForm({
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t("sshUnixGroupsDescription")} {t(
"sshUnixGroupsDescription"
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -697,8 +698,9 @@ export function RoleForm({
</FormItem> </FormItem>
)} )}
/> />
</div> </>
)} )}
</div>
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
-7
View File
@@ -1,10 +1,3 @@
/**
* Set a cookie on the client side in javascript code, not on the server
* @param name
* @param value
* @param days
* @param options
*/
export function setClientCookie( export function setClientCookie(
name: string, name: string,
value: string, value: string,