Remove advanced resources paywall

This commit is contained in:
Owen
2026-08-14 16:33:05 -04:00
parent 111d4b1b8c
commit 24998ce0a1
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();
@@ -1854,6 +1856,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,68 +155,56 @@ export default function PrivateResourceSshPage() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset <Form {...form}>
disabled={sshSectionDisabled} <SettingsSectionBody>
className={ <SettingsSectionForm variant="half">
sshSectionDisabled <SettingsFormGrid>
? "opacity-50 pointer-events-none" <SshServerSettingsFields
: "" idPrefix="private-ssh-edit"
} pamMode={pamMode}
> standardDaemonLocation={
<Form {...form}> standardDaemonLocation
<SettingsSectionBody> }
<SettingsSectionForm variant="half"> authDaemonPort={authDaemonPort}
<SettingsFormGrid> onPamModeChange={handlePamModeChange}
<SshServerSettingsFields onStandardDaemonLocationChange={
idPrefix="private-ssh-edit" handleDaemonLocationChange
pamMode={pamMode} }
standardDaemonLocation={ onAuthDaemonPortChange={(value) =>
standardDaemonLocation form.setValue("authDaemonPort", value, {
} shouldValidate: true
authDaemonPort={authDaemonPort} })
onPamModeChange={handlePamModeChange} }
onStandardDaemonLocationChange={ authDaemonPortError={
handleDaemonLocationChange form.formState.errors.authDaemonPort
} ?.message
onAuthDaemonPortChange={(value) => }
form.setValue( sshServerMode={sshServerMode}
"authDaemonPort", serverModeDisplay="badge"
value, />
{ shouldValidate: true } <PrivateResourceSshFields
) control={asAnyControl(form.control)}
} setValue={asAnySetValue(form.setValue)}
authDaemonPortError={ watch={asAnyWatch(form.watch)}
form.formState.errors.authDaemonPort orgId={siteResource.orgId}
?.message selectedSites={selectedSites}
} onSelectedSitesChange={setSelectedSites}
sshServerMode={sshServerMode} showSshSettings={false}
serverModeDisplay="badge" embedInParentGrid
/> isNativeSsh={isNative}
<PrivateResourceSshFields />
control={asAnyControl(form.control)} </SettingsFormGrid>
setValue={asAnySetValue(form.setValue)} </SettingsSectionForm>
watch={asAnyWatch(form.watch)} </SettingsSectionBody>
orgId={siteResource.orgId}
selectedSites={selectedSites}
onSelectedSitesChange={setSelectedSites}
showSshSettings={false}
embedInParentGrid
showPaidFeaturesAlert={false}
isNativeSsh={isNative}
/>
</SettingsFormGrid>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter> <SettingsSectionFooter>
<form action={formAction}> <form action={formAction}>
<Button type="submit" loading={saveLoading}> <Button type="submit" loading={saveLoading}>
{t("saveSettings")} {t("saveSettings")}
</Button> </Button>
</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,20 +150,16 @@ export default function CreatePrivateResourcePage() {
title: t("createInternalResourceDialogModeCidr"), title: t("createInternalResourceDialogModeCidr"),
description: t("privateResourceTypeCidrDescription") description: t("privateResourceTypeCidrDescription")
}, },
...(!disableEnterpriseFeatures {
? [ value: "http" as const,
{ title: t("createInternalResourceDialogModeHttp"),
value: "http" as const, description: t("privateResourceTypeHttpDescription")
title: t("createInternalResourceDialogModeHttp"), },
description: t("privateResourceTypeHttpDescription") {
}, value: "ssh" as const,
{ title: t("createInternalResourceDialogModeSsh"),
value: "ssh" as const, description: t("privateResourceTypeSshDescription")
title: t("createInternalResourceDialogModeSsh"), },
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,62 +574,44 @@ export default function CreatePrivateResourcePage() {
)} )}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<fieldset
disabled={httpSectionDisabled} <SettingsSectionBody>
className={ <SettingsSectionForm variant="half">
httpSectionDisabled <SettingsFormGrid>
? "opacity-50 pointer-events-none" <SettingsFormCell span="half">
: "" <PrivateResourceSitesField
} control={form.control}
> orgId={orgId}
<SettingsSectionBody> selectedSites={selectedSites}
<SettingsSectionForm variant="half"> onSelectedSitesChange={
<SettingsFormGrid> setSelectedSites
<SettingsFormCell span="half"> }
<PrivateResourceSitesField />
control={form.control} </SettingsFormCell>
orgId={orgId} <SettingsFormCell span="full">
selectedSites={ <PrivateResourceHttpFields
selectedSites control={asAnyControl(
} form.control
onSelectedSitesChange={ )}
setSelectedSites setValue={asAnySetValue(
} form.setValue
/> )}
</SettingsFormCell> orgId={orgId}
<SettingsFormCell span="full"> watch={asAnyWatch(form.watch)}
<PrivateResourceHttpFields disabled={true}
control={asAnyControl( labelPrefix="create"
form.control hideDomainPicker
)} />
setValue={asAnySetValue( </SettingsFormCell>
form.setValue </SettingsFormGrid>
)} </SettingsSectionForm>
orgId={orgId} </SettingsSectionBody>
watch={asAnyWatch(
form.watch
)}
disabled={
httpSectionDisabled
}
labelPrefix="create"
hideDomainPicker
hidePaidFeaturesAlert
/>
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionForm>
</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 <SettingsSectionBody>
disabled={sshSectionDisabled} <SettingsSectionForm variant="half">
className={ <PrivateResourceSshFields
sshSectionDisabled control={asAnyControl(form.control)}
? "opacity-50 pointer-events-none" setValue={asAnySetValue(form.setValue)}
: "" watch={asAnyWatch(form.watch)}
} orgId={orgId}
> disabled={false}
<SettingsSectionBody> selectedSites={selectedSites}
<SettingsSectionForm variant="half"> onSelectedSitesChange={setSelectedSites}
<PrivateResourceSshFields labelPrefix="create"
control={asAnyControl(form.control)} showSshSettings={true}
setValue={asAnySetValue( layout="wizard"
form.setValue hideAlias
)} />
watch={asAnyWatch(form.watch)} </SettingsSectionForm>
orgId={orgId} </SettingsSectionBody>
disabled={sshSectionDisabled}
selectedSites={selectedSites}
onSelectedSitesChange={
setSelectedSites
}
labelPrefix="create"
showSshSettings={true}
layout="wizard"
showPaidFeaturesAlert={false}
hideAlias
/>
</SettingsSectionForm>
</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}
</> </>
+185 -183
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,115 +481,161 @@ export function RoleForm({
/> />
</div> </div>
{/* SSH tab - hidden when enterprise features are disabled */} <div className="space-y-4 mt-4">
{!env.flags.disableEnterpriseFeatures && ( <FormField
<div className="space-y-4 mt-4"> control={form.control}
<PaidFeaturesAlert name="allowSsh"
tiers={tierMatrix.advancedPrivateResources} render={({ field }) => {
/> const allowSshOptions: OptionSelectOption<
<FormField "allow" | "disallow"
control={form.control} >[] = [
name="allowSsh" {
render={({ field }) => { value: "allow",
const allowSshOptions: OptionSelectOption< label: t("roleAllowSshAllow")
"allow" | "disallow" },
>[] = [ {
{ value: "disallow",
value: "allow", label: t("roleAllowSshDisallow")
label: t("roleAllowSshAllow") }
}, ];
{ return (
value: "disallow", <FormItem>
label: t("roleAllowSshDisallow") <FormLabel>
} {t("roleAllowSsh")}
]; </FormLabel>
return ( <OptionSelect<"allow" | "disallow">
<FormItem> options={allowSshOptions}
<FormLabel> value={
{t("roleAllowSsh")} sshDisabled
</FormLabel> ? "disallow"
<OptionSelect< : field.value
"allow" | "disallow" ? "allow"
> : "disallow"
options={allowSshOptions}
value={
sshDisabled
? "disallow"
: field.value
? "allow"
: "disallow"
}
onChange={(v) => {
if (sshDisabled) return;
field.onChange(
v === "allow"
);
}}
cols={2}
disabled={sshDisabled}
/>
<FormDescription>
{t(
"roleAllowSshDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="sshSudoMode"
render={({ field }) => {
const sudoOptions: OptionSelectOption<SshSudoMode>[] =
[
{
value: "none",
label: t("sshSudoModeNone")
},
{
value: "full",
label: t("sshSudoModeFull")
},
{
value: "commands",
label: t(
"sshSudoModeCommands"
)
} }
]; onChange={(v) => {
return ( if (sshDisabled) return;
<FormItem> field.onChange(
<FormLabel> v === "allow"
{t("sshSudoMode")} );
</FormLabel> }}
<OptionSelect<SshSudoMode> cols={2}
options={sudoOptions} disabled={sshDisabled}
value={field.value} />
onChange={field.onChange} <FormDescription>
cols={3} {t("roleAllowSshDescription")}
disabled={sshDisabled} </FormDescription>
/> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> );
); }}
}} />
/> {/* SSH tab - hidden when enterprise features are disabled */}
{sshSudoMode === "commands" && ( {!env.flags.disableEnterpriseFeatures && (
<>
<PaidFeaturesAlert
tiers={tierMatrix.roleBasedSSHControls}
/>
<FormField <FormField
control={form.control} control={form.control}
name="sshSudoCommands" name="sshSudoMode"
render={({ field }) => {
const sudoOptions: OptionSelectOption<SshSudoMode>[] =
[
{
value: "none",
label: t(
"sshSudoModeNone"
)
},
{
value: "full",
label: t(
"sshSudoModeFull"
)
},
{
value: "commands",
label: t(
"sshSudoModeCommands"
)
}
];
return (
<FormItem>
<FormLabel>
{t("sshSudoMode")}
</FormLabel>
<OptionSelect<SshSudoMode>
options={sudoOptions}
value={field.value}
onChange={
field.onChange
}
cols={3}
disabled={sshDisabled}
/>
<FormMessage />
</FormItem>
);
}}
/>
{sshSudoMode === "commands" && (
<FormField
control={form.control}
name="sshSudoCommands"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("sshSudoCommands")}
</FormLabel>
<FormControl>
<Textarea
{...field}
{...getTextImportDropHandlers(
"sshSudoCommands"
)}
placeholder={
sshDisabled
? undefined
: t(
"roleTextFieldPlaceholder"
)
}
disabled={
sshDisabled
}
className={cn(
"h-20 min-h-20",
dragOverField ===
"sshSudoCommands" &&
"border-primary"
)}
/>
</FormControl>
<FormDescription>
{t(
"sshSudoCommandsDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="sshUnixGroups"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t("sshSudoCommands")} {t("sshUnixGroups")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
{...field} {...field}
{...getTextImportDropHandlers( {...getTextImportDropHandlers(
"sshSudoCommands" "sshUnixGroups"
)} )}
placeholder={ placeholder={
sshDisabled sshDisabled
@@ -608,97 +648,59 @@ export function RoleForm({
className={cn( className={cn(
"h-20 min-h-20", "h-20 min-h-20",
dragOverField === dragOverField ===
"sshSudoCommands" && "sshUnixGroups" &&
"border-primary" "border-primary"
)} )}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t( {t(
"sshSudoCommandsDescription" "sshUnixGroupsDescription"
)} )}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
)}
<FormField <FormField
control={form.control} control={form.control}
name="sshUnixGroups" name="sshCreateHomeDir"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="my-2">
<FormLabel> <FormControl>
{t("sshUnixGroups")} <CheckboxWithLabel
</FormLabel> {...field}
<FormControl> value="on"
<Textarea checked={form.watch(
{...field} "sshCreateHomeDir"
{...getTextImportDropHandlers( )}
"sshUnixGroups" onCheckedChange={(
)} checked
placeholder={ ) => {
sshDisabled if (
? undefined checked !==
: t( "indeterminate"
"roleTextFieldPlaceholder" ) {
) form.setValue(
} "sshCreateHomeDir",
disabled={sshDisabled} checked
className={cn( );
"h-20 min-h-20", }
dragOverField === }}
"sshUnixGroups" && label={t(
"border-primary" "sshCreateHomeDir"
)} )}
/> disabled={sshDisabled}
</FormControl> />
<FormDescription> </FormControl>
{t("sshUnixGroupsDescription")} <FormMessage />
</FormDescription> </FormItem>
<FormMessage /> )}
</FormItem> />
)} </>
/> )}
</div>
<FormField
control={form.control}
name="sshCreateHomeDir"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
value="on"
checked={form.watch(
"sshCreateHomeDir"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"sshCreateHomeDir",
checked
);
}
}}
label={t(
"sshCreateHomeDir"
)}
disabled={sshDisabled}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</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,