mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-15 08:49:59 +02:00
Remove advanced resources paywall
This commit is contained in:
@@ -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"]
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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,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
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user