mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-07 21:19:03 +02:00
Merge branch 'dev' into refactor/standardize-dropdowns
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, users } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
type SetServerAdminArgs = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
|
||||||
|
command: "set-server-admin",
|
||||||
|
describe: "Mark any user as a server admin by email address",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs.option("email", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "User email address"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { email: string }) => {
|
||||||
|
try {
|
||||||
|
const email = argv.email.trim().toLowerCase();
|
||||||
|
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error(`User with email '${email}' not found`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.serverAdmin) {
|
||||||
|
console.log(`User '${email}' is already a server admin`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({ serverAdmin: true })
|
||||||
|
.where(eq(users.userId, user.userId));
|
||||||
|
|
||||||
|
console.log(`User '${email}' has been marked as a server admin`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ import { deleteClient } from "./commands/deleteClient";
|
|||||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||||
import { clearCertificates } from "./commands/clearCertificates";
|
import { clearCertificates } from "./commands/clearCertificates";
|
||||||
import { disableUser2fa } from "./commands/disableUser2fa";
|
import { disableUser2fa } from "./commands/disableUser2fa";
|
||||||
|
import { setServerAdmin } from "./commands/setServerAdmin";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -23,5 +24,6 @@ yargs(hideBin(process.argv))
|
|||||||
.command(generateOrgCaKeys)
|
.command(generateOrgCaKeys)
|
||||||
.command(clearCertificates)
|
.command(clearCertificates)
|
||||||
.command(disableUser2fa)
|
.command(disableUser2fa)
|
||||||
|
.command(setServerAdmin)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -26,15 +26,22 @@ import {
|
|||||||
userPolicies,
|
userPolicies,
|
||||||
users,
|
users,
|
||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
resourceHeaderAuthExtendedCompatibility
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
|
resourcePolicies,
|
||||||
|
resourcePolicyPincode,
|
||||||
|
ResourcePolicyPincode,
|
||||||
|
resourcePolicyPassword,
|
||||||
|
ResourcePolicyPassword,
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
ResourcePolicyHeaderAuth
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq, inArray, or, sql } from "drizzle-orm";
|
import { and, eq, inArray, or, sql } from "drizzle-orm";
|
||||||
|
|
||||||
export type ResourceWithAuth = {
|
export type ResourceWithAuth = {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | ResourcePolicyPincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | ResourcePolicyPassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
@@ -82,6 +89,31 @@ export async function getResourceByDomain(
|
|||||||
resources.resourceId
|
resources.resourceId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicies,
|
||||||
|
eq(resourcePolicies.resourcePolicyId, resources.resourcePolicyId)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPincode,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPincode.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPassword,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPassword.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
eq(
|
||||||
|
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(
|
.where(
|
||||||
or(
|
or(
|
||||||
@@ -113,11 +145,18 @@ export async function getResourceByDomain(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
resource: result.resources,
|
resource: result.resources,
|
||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePolicyPincode ?? result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePolicyPassword ?? result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth,
|
headerAuth:
|
||||||
headerAuthExtendedCompatibility:
|
result.resourcePolicyHeaderAuth ?? result.resourceHeaderAuth,
|
||||||
result.resourceHeaderAuthExtendedCompatibility,
|
headerAuthExtendedCompatibility: result.resourcePolicyHeaderAuth
|
||||||
|
? ({
|
||||||
|
headerAuthExtendedCompatibilityId: 0,
|
||||||
|
resourceId: result.resources.resourceId,
|
||||||
|
extendedCompatibilityIsActivated:
|
||||||
|
result.resourcePolicyHeaderAuth.extendedCompatibility
|
||||||
|
} as ResourceHeaderAuthExtendedCompatibility)
|
||||||
|
: result.resourceHeaderAuthExtendedCompatibility,
|
||||||
org: result.orgs
|
org: result.orgs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1545,5 +1545,14 @@ export type RoundTripMessageTracker = InferSelectModel<
|
|||||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||||
export type Label = InferSelectModel<typeof labels>;
|
export type Label = InferSelectModel<typeof labels>;
|
||||||
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||||
|
export type ResourcePolicyPincode = InferSelectModel<
|
||||||
|
typeof resourcePolicyPincode
|
||||||
|
>;
|
||||||
|
export type ResourcePolicyPassword = InferSelectModel<
|
||||||
|
typeof resourcePolicyPassword
|
||||||
|
>;
|
||||||
|
export type ResourcePolicyHeaderAuth = InferSelectModel<
|
||||||
|
typeof resourcePolicyHeaderAuth
|
||||||
|
>;
|
||||||
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||||
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||||
|
|||||||
@@ -16,18 +16,18 @@ export enum TierFeature {
|
|||||||
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
|
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
|
||||||
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
|
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
|
||||||
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
|
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
|
||||||
SshPam = "sshPam",
|
|
||||||
FullRbac = "fullRbac",
|
FullRbac = "fullRbac",
|
||||||
SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
|
SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
|
||||||
SIEM = "siem", // handle downgrade by disabling SIEM integrations
|
SIEM = "siem", // handle downgrade by disabling SIEM integrations
|
||||||
HTTPPrivateResources = "httpPrivateResources", // handle downgrade by disabling HTTP private resources
|
|
||||||
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
||||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||||
AlertingRules = "alertingRules",
|
AlertingRules = "alertingRules",
|
||||||
WildcardSubdomain = "wildcardSubdomain",
|
WildcardSubdomain = "wildcardSubdomain",
|
||||||
Labels = "labels",
|
Labels = "labels",
|
||||||
NewtAutoUpdate = "newtAutoUpdate",
|
NewtAutoUpdate = "newtAutoUpdate",
|
||||||
ResourcePolicies = "resourcePolicies"
|
ResourcePolicies = "resourcePolicies",
|
||||||
|
AdvancedPublicResources = "advancedPublicResources",
|
||||||
|
AdvancedPrivateResources = "advancedPrivateResources"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -62,15 +62,25 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
"enterprise"
|
"enterprise"
|
||||||
],
|
],
|
||||||
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
|
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
|
||||||
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
|
|
||||||
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
|
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
|
||||||
[TierFeature.SIEM]: ["enterprise"],
|
[TierFeature.SIEM]: ["enterprise"],
|
||||||
[TierFeature.HTTPPrivateResources]: ["tier3", "enterprise"],
|
|
||||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
||||||
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
||||||
[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]: [
|
||||||
|
"tier1",
|
||||||
|
"tier2",
|
||||||
|
"tier3",
|
||||||
|
"enterprise"
|
||||||
|
],
|
||||||
|
[TierFeature.AdvancedPrivateResources]: [
|
||||||
|
"tier1",
|
||||||
|
"tier2",
|
||||||
|
"tier3",
|
||||||
|
"enterprise"
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -337,6 +337,15 @@ export async function updateProxyResources(
|
|||||||
resourceData.maintenance?.message,
|
resourceData.maintenance?.message,
|
||||||
maintenanceEstimatedTime:
|
maintenanceEstimatedTime:
|
||||||
resourceData.maintenance?.["estimated-time"],
|
resourceData.maintenance?.["estimated-time"],
|
||||||
|
proxyProtocol:
|
||||||
|
resourceData.mode === "tcp"
|
||||||
|
? (resourceData["proxy-protocol"] ?? false)
|
||||||
|
: false,
|
||||||
|
proxyProtocolVersion:
|
||||||
|
resourceData.mode === "tcp"
|
||||||
|
? (resourceData["proxy-protocol-version"] ??
|
||||||
|
1)
|
||||||
|
: 1,
|
||||||
resourcePolicyId: sharedPolicy.resourcePolicyId
|
resourcePolicyId: sharedPolicy.resourcePolicyId
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
@@ -504,6 +513,15 @@ export async function updateProxyResources(
|
|||||||
resourceData.maintenance?.message,
|
resourceData.maintenance?.message,
|
||||||
maintenanceEstimatedTime:
|
maintenanceEstimatedTime:
|
||||||
resourceData.maintenance?.["estimated-time"],
|
resourceData.maintenance?.["estimated-time"],
|
||||||
|
proxyProtocol:
|
||||||
|
resourceData.mode === "tcp"
|
||||||
|
? (resourceData["proxy-protocol"] ?? false)
|
||||||
|
: false,
|
||||||
|
proxyProtocolVersion:
|
||||||
|
resourceData.mode === "tcp"
|
||||||
|
? (resourceData["proxy-protocol-version"] ??
|
||||||
|
1)
|
||||||
|
: 1,
|
||||||
resourcePolicyId: null,
|
resourcePolicyId: null,
|
||||||
defaultResourcePolicyId: inlinePolicyId
|
defaultResourcePolicyId: inlinePolicyId
|
||||||
})
|
})
|
||||||
@@ -994,6 +1012,14 @@ export async function updateProxyResources(
|
|||||||
maintenanceMessage: resourceData.maintenance?.message,
|
maintenanceMessage: resourceData.maintenance?.message,
|
||||||
maintenanceEstimatedTime:
|
maintenanceEstimatedTime:
|
||||||
resourceData.maintenance?.["estimated-time"],
|
resourceData.maintenance?.["estimated-time"],
|
||||||
|
proxyProtocol:
|
||||||
|
resourceData.mode === "tcp"
|
||||||
|
? (resourceData["proxy-protocol"] ?? false)
|
||||||
|
: false,
|
||||||
|
proxyProtocolVersion:
|
||||||
|
resourceData.mode === "tcp"
|
||||||
|
? (resourceData["proxy-protocol-version"] ?? 1)
|
||||||
|
: 1,
|
||||||
defaultResourcePolicyId: inlinePolicy.resourcePolicyId,
|
defaultResourcePolicyId: inlinePolicy.resourcePolicyId,
|
||||||
resourcePolicyId: sharedPolicyId,
|
resourcePolicyId: sharedPolicyId,
|
||||||
// Only set these resource-level fields when using a shared policy
|
// Only set these resource-level fields when using a shared policy
|
||||||
@@ -1231,7 +1257,9 @@ async function syncRoleResources(
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
role = created;
|
role = created;
|
||||||
logger.info(`Auto-created role "${roleName}" in org ${orgId} from blueprint`);
|
logger.info(
|
||||||
|
`Auto-created role "${roleName}" in org ${orgId} from blueprint`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role.isAdmin) {
|
if (role.isAdmin) {
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ export const PublicResourceSchema = z
|
|||||||
headers: z.array(HeaderSchema).optional(),
|
headers: z.array(HeaderSchema).optional(),
|
||||||
rules: z.array(RuleSchema).optional(),
|
rules: z.array(RuleSchema).optional(),
|
||||||
maintenance: MaintenanceSchema.optional(),
|
maintenance: MaintenanceSchema.optional(),
|
||||||
"auth-daemon": AuthDaemonSchema.optional()
|
"auth-daemon": AuthDaemonSchema.optional(),
|
||||||
|
"proxy-protocol": z.boolean().optional(),
|
||||||
|
"proxy-protocol-version": z.int().min(1).optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(resource) => {
|
(resource) => {
|
||||||
@@ -378,6 +380,23 @@ export const PublicResourceSchema = z
|
|||||||
'Wildcard full-domain must have "*" as the leftmost label only, followed by at least two valid hostname labels (e.g. "*.example.com" or "*.level1.example.com"). Patterns like "*example.com" or "level2.*.example.com" are not supported.'
|
'Wildcard full-domain must have "*" as the leftmost label only, followed by at least two valid hostname labels (e.g. "*.example.com" or "*.level1.example.com"). Patterns like "*example.com" or "level2.*.example.com" are not supported.'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
const effectiveMode = resource.mode ?? resource.protocol;
|
||||||
|
if (effectiveMode !== "tcp") {
|
||||||
|
return (
|
||||||
|
resource["proxy-protocol"] === undefined &&
|
||||||
|
resource["proxy-protocol-version"] === undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["proxy-protocol"],
|
||||||
|
message:
|
||||||
|
"'proxy-protocol' and 'proxy-protocol-version' can only be set when mode is 'tcp'"
|
||||||
|
}
|
||||||
|
)
|
||||||
.transform((resource) => {
|
.transform((resource) => {
|
||||||
// Normalize: prefer mode, fall back to protocol for backwards compatibility
|
// Normalize: prefer mode, fall back to protocol for backwards compatibility
|
||||||
if (resource.mode === undefined && resource.protocol !== undefined) {
|
if (resource.mode === undefined && resource.protocol !== undefined) {
|
||||||
|
|||||||
@@ -308,8 +308,8 @@ async function disableFeature(
|
|||||||
await disableAutoProvisioning(orgId);
|
await disableAutoProvisioning(orgId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TierFeature.SshPam:
|
case TierFeature.AdvancedPrivateResources:
|
||||||
await disableSshPam(orgId);
|
await disableAdvancedPrivateResources(orgId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TierFeature.FullRbac:
|
case TierFeature.FullRbac:
|
||||||
@@ -357,10 +357,11 @@ 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 disableSshPam(orgId: string): Promise<void> {
|
async function disableAdvancedPrivateResources(orgId: string): Promise<void> {
|
||||||
logger.info(
|
// TODO: implement logic to disable advanced private resourcs like ssh and ssh pam
|
||||||
`Disabled SSH PAM options on all roles and site resources for org ${orgId}`
|
// 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> {
|
||||||
|
|||||||
@@ -610,7 +610,7 @@ authenticated.put(
|
|||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/ssh/sign-key",
|
"/org/:orgId/ssh/sign-key",
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
verifyValidSubscription(tierMatrix.sshPam),
|
verifyValidSubscription(tierMatrix.advancedPrivateResources),
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyUserHasAction(ActionsEnum.signSshKey),
|
verifyUserHasAction(ActionsEnum.signSshKey),
|
||||||
|
|||||||
@@ -35,7 +35,14 @@ import {
|
|||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
orgs,
|
orgs,
|
||||||
requestAuditLog,
|
requestAuditLog,
|
||||||
Org
|
Org,
|
||||||
|
resourcePolicies,
|
||||||
|
resourcePolicyPincode,
|
||||||
|
ResourcePolicyPincode,
|
||||||
|
resourcePolicyPassword,
|
||||||
|
ResourcePolicyPassword,
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
ResourcePolicyHeaderAuth
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
resources,
|
resources,
|
||||||
@@ -204,9 +211,9 @@ export type ValidateResourceSessionTokenBody = z.infer<
|
|||||||
// Type definitions for API responses
|
// Type definitions for API responses
|
||||||
export type ResourceWithAuth = {
|
export type ResourceWithAuth = {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | ResourcePolicyPincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | ResourcePolicyPassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
@@ -529,6 +536,34 @@ hybridRouter.get(
|
|||||||
resources.resourceId
|
resources.resourceId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicies,
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
resources.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPincode,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPincode.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPassword,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPassword.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
eq(
|
||||||
|
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(
|
.where(
|
||||||
or(
|
or(
|
||||||
@@ -581,11 +616,21 @@ hybridRouter.get(
|
|||||||
|
|
||||||
const resourceWithAuth: ResourceWithAuth = {
|
const resourceWithAuth: ResourceWithAuth = {
|
||||||
resource: result.resources,
|
resource: result.resources,
|
||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePolicyPincode ?? result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password:
|
||||||
headerAuth: result.resourceHeaderAuth,
|
result.resourcePolicyPassword ?? result.resourcePassword,
|
||||||
headerAuthExtendedCompatibility:
|
headerAuth:
|
||||||
result.resourceHeaderAuthExtendedCompatibility,
|
result.resourcePolicyHeaderAuth ??
|
||||||
|
result.resourceHeaderAuth,
|
||||||
|
headerAuthExtendedCompatibility: result.resourcePolicyHeaderAuth
|
||||||
|
? ({
|
||||||
|
headerAuthExtendedCompatibilityId: 0,
|
||||||
|
resourceId: result.resources.resourceId,
|
||||||
|
extendedCompatibilityIsActivated:
|
||||||
|
result.resourcePolicyHeaderAuth
|
||||||
|
.extendedCompatibility
|
||||||
|
} as ResourceHeaderAuthExtendedCompatibility)
|
||||||
|
: result.resourceHeaderAuthExtendedCompatibility,
|
||||||
org: result.orgs
|
org: result.orgs
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -78,41 +78,9 @@ export type SignSshKeyResponse = {
|
|||||||
validAfter?: string;
|
validAfter?: string;
|
||||||
validBefore?: string;
|
validBefore?: string;
|
||||||
expiresIn?: number;
|
expiresIn?: number;
|
||||||
|
authDaemonMode: "site" | "remote" | "native" | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// registry.registerPath({
|
|
||||||
// method: "post",
|
|
||||||
// path: "/org/{orgId}/ssh/sign-key",
|
|
||||||
// description: "Sign an SSH public key for access to a resource.",
|
|
||||||
// tags: [OpenAPITags.Org, OpenAPITags.Ssh],
|
|
||||||
// request: {
|
|
||||||
// params: paramsSchema,
|
|
||||||
// body: {
|
|
||||||
// content: {
|
|
||||||
// "application/json": {
|
|
||||||
// schema: bodySchema
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// responses: {
|
|
||||||
// 200: {
|
|
||||||
// description: "Successful response",
|
|
||||||
// content: {
|
|
||||||
// "application/json": {
|
|
||||||
// schema: z.object({
|
|
||||||
// data: z.unknown().nullable(),
|
|
||||||
// success: z.boolean(),
|
|
||||||
// error: z.boolean(),
|
|
||||||
// message: z.string(),
|
|
||||||
// status: z.number()
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
export async function signSshKey(
|
export async function signSshKey(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -181,7 +149,7 @@ export async function signSshKey(
|
|||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.sshPam
|
tierMatrix.advancedPrivateResources
|
||||||
);
|
);
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
return next(
|
return next(
|
||||||
@@ -654,6 +622,7 @@ export async function signSshKey(
|
|||||||
siteIds: siteIds,
|
siteIds: siteIds,
|
||||||
siteId: siteIds[0], // just pick the first one for backward compatibility with older olms
|
siteId: siteIds[0], // just pick the first one for backward compatibility with older olms
|
||||||
keyId: cert?.keyId,
|
keyId: cert?.keyId,
|
||||||
|
authDaemonMode: resource.authDaemonMode,
|
||||||
validPrincipals: cert?.validPrincipals,
|
validPrincipals: cert?.validPrincipals,
|
||||||
validAfter: cert?.validAfter.toISOString(),
|
validAfter: cert?.validAfter.toISOString(),
|
||||||
validBefore: cert?.validBefore.toISOString(),
|
validBefore: cert?.validBefore.toISOString(),
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import {
|
|||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
ResourcePincode,
|
ResourcePincode,
|
||||||
|
ResourcePolicyPincode,
|
||||||
|
ResourcePolicyPassword,
|
||||||
|
ResourcePolicyHeaderAuth,
|
||||||
ResourceRule
|
ResourceRule
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -134,9 +137,12 @@ export async function verifyResourceSession(
|
|||||||
let resourceData:
|
let resourceData:
|
||||||
| {
|
| {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | ResourcePolicyPincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | ResourcePolicyPassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth:
|
||||||
|
| ResourceHeaderAuth
|
||||||
|
| ResourcePolicyHeaderAuth
|
||||||
|
| null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
}
|
}
|
||||||
@@ -577,7 +583,11 @@ export async function verifyResourceSession(
|
|||||||
return notAllowed(res, redirectPath, resource.orgId);
|
return notAllowed(res, redirectPath, resource.orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pincode && resourceSession.pincodeId) {
|
if (
|
||||||
|
pincode &&
|
||||||
|
(resourceSession.pincodeId ||
|
||||||
|
resourceSession.policyPincodeId)
|
||||||
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Resource allowed because pincode session is valid"
|
"Resource allowed because pincode session is valid"
|
||||||
);
|
);
|
||||||
@@ -596,7 +606,11 @@ export async function verifyResourceSession(
|
|||||||
return allowed(res, undefined, dontStripSession);
|
return allowed(res, undefined, dontStripSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password && resourceSession.passwordId) {
|
if (
|
||||||
|
password &&
|
||||||
|
(resourceSession.passwordId ||
|
||||||
|
resourceSession.policyPasswordId)
|
||||||
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Resource allowed because password session is valid"
|
"Resource allowed because password session is valid"
|
||||||
);
|
);
|
||||||
@@ -617,7 +631,8 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
resource.emailWhitelistEnabled &&
|
resource.emailWhitelistEnabled &&
|
||||||
resourceSession.whitelistId
|
(resourceSession.whitelistId ||
|
||||||
|
resourceSession.policyWhitelistId)
|
||||||
) {
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Resource allowed because whitelist session is valid"
|
"Resource allowed because whitelist session is valid"
|
||||||
|
|||||||
@@ -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 { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import {
|
import {
|
||||||
getUniqueResourceName,
|
getUniqueResourceName,
|
||||||
getUniqueResourcePolicyName
|
getUniqueResourcePolicyName
|
||||||
@@ -342,6 +342,21 @@ async function createHttpResource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
["ssh", "rdp", "vnc"].includes(mode!) &&
|
||||||
|
!isLicensedOrSubscribed(
|
||||||
|
orgId!,
|
||||||
|
tierMatrix[TierFeature.AdvancedPublicResources]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Your current subscription does not support browser gateway resources. Please upgrade to access this feature."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Validate domain and construct full domain
|
// Validate domain and construct full domain
|
||||||
const domainResult = await validateAndConstructDomain(
|
const domainResult = await validateAndConstructDomain(
|
||||||
domainId,
|
domainId,
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ export async function getUserResources(
|
|||||||
return {
|
return {
|
||||||
siteResourceId: siteResource.siteResourceId,
|
siteResourceId: siteResource.siteResourceId,
|
||||||
name: siteResource.name,
|
name: siteResource.name,
|
||||||
|
niceId: siteResource.niceId,
|
||||||
destination: siteResource.destination,
|
destination: siteResource.destination,
|
||||||
mode: siteResource.mode,
|
mode: siteResource.mode,
|
||||||
ssl: siteResource.ssl,
|
ssl: siteResource.ssl,
|
||||||
@@ -492,6 +493,7 @@ export type GetUserResourcesResponse = {
|
|||||||
siteResources: Array<{
|
siteResources: Array<{
|
||||||
siteResourceId: number;
|
siteResourceId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
niceId: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
tcpPortRangeString: string | null;
|
tcpPortRangeString: string | null;
|
||||||
|
|||||||
@@ -123,23 +123,40 @@ export async function createRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix.deviceApprovals
|
||||||
|
);
|
||||||
if (!isLicensedDeviceApprovals) {
|
if (!isLicensedDeviceApprovals) {
|
||||||
roleData.requireDeviceApproval = undefined;
|
roleData.requireDeviceApproval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
|
const isLicensedSshPam = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix.advancedPrivateResources
|
||||||
|
);
|
||||||
const roleInsertValues: Record<string, unknown> = {
|
const roleInsertValues: Record<string, unknown> = {
|
||||||
name: roleData.name,
|
name: roleData.name,
|
||||||
orgId
|
orgId
|
||||||
};
|
};
|
||||||
if (roleData.description !== undefined) roleInsertValues.description = roleData.description;
|
if (roleData.description !== undefined)
|
||||||
if (roleData.requireDeviceApproval !== undefined) roleInsertValues.requireDeviceApproval = roleData.requireDeviceApproval;
|
roleInsertValues.description = roleData.description;
|
||||||
|
if (roleData.requireDeviceApproval !== undefined)
|
||||||
|
roleInsertValues.requireDeviceApproval =
|
||||||
|
roleData.requireDeviceApproval;
|
||||||
if (isLicensedSshPam) {
|
if (isLicensedSshPam) {
|
||||||
if (roleData.sshSudoMode !== undefined) roleInsertValues.sshSudoMode = roleData.sshSudoMode;
|
if (roleData.sshSudoMode !== undefined)
|
||||||
if (roleData.sshSudoCommands !== undefined) roleInsertValues.sshSudoCommands = JSON.stringify(roleData.sshSudoCommands);
|
roleInsertValues.sshSudoMode = roleData.sshSudoMode;
|
||||||
if (roleData.sshCreateHomeDir !== undefined) roleInsertValues.sshCreateHomeDir = roleData.sshCreateHomeDir;
|
if (roleData.sshSudoCommands !== undefined)
|
||||||
if (roleData.sshUnixGroups !== undefined) roleInsertValues.sshUnixGroups = JSON.stringify(roleData.sshUnixGroups);
|
roleInsertValues.sshSudoCommands = JSON.stringify(
|
||||||
|
roleData.sshSudoCommands
|
||||||
|
);
|
||||||
|
if (roleData.sshCreateHomeDir !== undefined)
|
||||||
|
roleInsertValues.sshCreateHomeDir = roleData.sshCreateHomeDir;
|
||||||
|
if (roleData.sshUnixGroups !== undefined)
|
||||||
|
roleInsertValues.sshUnixGroups = JSON.stringify(
|
||||||
|
roleData.sshUnixGroups
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
|
|||||||
@@ -134,12 +134,18 @@ export async function updateRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix.deviceApprovals
|
||||||
|
);
|
||||||
if (!isLicensedDeviceApprovals) {
|
if (!isLicensedDeviceApprovals) {
|
||||||
updateData.requireDeviceApproval = undefined;
|
updateData.requireDeviceApproval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
|
const isLicensedSshPam = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix.advancedPrivateResources
|
||||||
|
);
|
||||||
if (!isLicensedSshPam) {
|
if (!isLicensedSshPam) {
|
||||||
delete updateData.sshSudoMode;
|
delete updateData.sshSudoMode;
|
||||||
delete updateData.sshSudoCommands;
|
delete updateData.sshSudoCommands;
|
||||||
@@ -147,10 +153,14 @@ export async function updateRole(
|
|||||||
delete updateData.sshUnixGroups;
|
delete updateData.sshUnixGroups;
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(updateData.sshSudoCommands)) {
|
if (Array.isArray(updateData.sshSudoCommands)) {
|
||||||
updateData.sshSudoCommands = JSON.stringify(updateData.sshSudoCommands);
|
updateData.sshSudoCommands = JSON.stringify(
|
||||||
|
updateData.sshSudoCommands
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (Array.isArray(updateData.sshUnixGroups)) {
|
if (Array.isArray(updateData.sshUnixGroups)) {
|
||||||
updateData.sshUnixGroups = JSON.stringify(updateData.sshUnixGroups);
|
updateData.sshUnixGroups = JSON.stringify(
|
||||||
|
updateData.sshUnixGroups
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ const createSiteResourceSchema = z
|
|||||||
scheme: z.enum(["http", "https"]).optional(),
|
scheme: z.enum(["http", "https"]).optional(),
|
||||||
siteIds: z.array(z.int()).optional(),
|
siteIds: z.array(z.int()).optional(),
|
||||||
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
|
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
|
||||||
// proxyPort: z.int().positive().optional(),
|
|
||||||
destinationPort: z.int().positive().optional(),
|
destinationPort: z.int().positive().optional(),
|
||||||
destination: z.string().min(1).optional(),
|
destination: z.string().min(1).optional(),
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
@@ -174,6 +173,25 @@ const createSiteResourceSchema = z
|
|||||||
{
|
{
|
||||||
message: "At least one of siteIds or siteId must be provided"
|
message: "At least one of siteIds or siteId must be provided"
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode !== "ssh") return true;
|
||||||
|
const isSingleSiteMode =
|
||||||
|
data.authDaemonMode === "native" ||
|
||||||
|
(data.pamMode === "push" && data.authDaemonMode === "site") ||
|
||||||
|
(data.pamMode === "push" && data.authDaemonMode === undefined);
|
||||||
|
if (!isSingleSiteMode) return true;
|
||||||
|
const effectiveSiteIds = [
|
||||||
|
...(data.siteIds ?? []),
|
||||||
|
...(data.siteId !== undefined ? [data.siteId] : [])
|
||||||
|
];
|
||||||
|
const uniqueSiteIds = new Set(effectiveSiteIds);
|
||||||
|
return uniqueSiteIds.size <= 1;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Only one site is allowed for this SSH daemon mode"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
|
export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
|
||||||
@@ -248,7 +266,6 @@ export async function createSiteResource(
|
|||||||
siteId,
|
siteId,
|
||||||
mode,
|
mode,
|
||||||
scheme,
|
scheme,
|
||||||
// proxyPort,
|
|
||||||
destinationPort,
|
destinationPort,
|
||||||
destination,
|
destination,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -276,7 +293,7 @@ export async function createSiteResource(
|
|||||||
if (mode == "http") {
|
if (mode == "http") {
|
||||||
const hasHttpFeature = await isLicensedOrSubscribed(
|
const hasHttpFeature = await isLicensedOrSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix[TierFeature.HTTPPrivateResources]
|
tierMatrix[TierFeature.AdvancedPrivateResources]
|
||||||
);
|
);
|
||||||
if (!hasHttpFeature) {
|
if (!hasHttpFeature) {
|
||||||
return next(
|
return next(
|
||||||
@@ -408,9 +425,18 @@ export async function createSiteResource(
|
|||||||
|
|
||||||
const isLicensedSshPam = await isLicensedOrSubscribed(
|
const isLicensedSshPam = await isLicensedOrSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.sshPam
|
tierMatrix.advancedPrivateResources
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (mode == "ssh" && !isLicensedSshPam) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"SSH private resources are not included in your current plan. Please upgrade."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let updatedNiceId = niceId;
|
let updatedNiceId = niceId;
|
||||||
if (!niceId) {
|
if (!niceId) {
|
||||||
updatedNiceId = await getUniqueSiteResourceName(orgId);
|
updatedNiceId = await getUniqueSiteResourceName(orgId);
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ const updateSiteResourceSchema = z
|
|||||||
mode: z.enum(["host", "cidr", "http", "ssh"]).optional(),
|
mode: z.enum(["host", "cidr", "http", "ssh"]).optional(),
|
||||||
ssl: z.boolean().optional(),
|
ssl: z.boolean().optional(),
|
||||||
scheme: z.enum(["http", "https"]).nullish(),
|
scheme: z.enum(["http", "https"]).nullish(),
|
||||||
// proxyPort: z.int().positive().nullish(),
|
|
||||||
destinationPort: z.int().positive().nullish(),
|
destinationPort: z.int().positive().nullish(),
|
||||||
destination: z.string().min(1).optional(),
|
destination: z.string().min(1).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
@@ -182,6 +181,25 @@ const updateSiteResourceSchema = z
|
|||||||
{
|
{
|
||||||
message: "At least one of siteIds or siteId must be provided"
|
message: "At least one of siteIds or siteId must be provided"
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode !== "ssh") return true;
|
||||||
|
const isSingleSiteMode =
|
||||||
|
data.authDaemonMode === "native" ||
|
||||||
|
(data.pamMode === "push" && data.authDaemonMode === "site") ||
|
||||||
|
(data.pamMode === "push" && data.authDaemonMode === undefined);
|
||||||
|
if (!isSingleSiteMode) return true;
|
||||||
|
const effectiveSiteIds = [
|
||||||
|
...(data.siteIds ?? []),
|
||||||
|
...(data.siteId !== undefined ? [data.siteId] : [])
|
||||||
|
];
|
||||||
|
const uniqueSiteIds = new Set(effectiveSiteIds);
|
||||||
|
return uniqueSiteIds.size <= 1;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Only one site is allowed for this SSH daemon mode"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
export type UpdateSiteResourceBody = z.infer<typeof updateSiteResourceSchema>;
|
||||||
@@ -296,7 +314,7 @@ export async function updateSiteResource(
|
|||||||
if (mode == "http") {
|
if (mode == "http") {
|
||||||
const hasHttpFeature = await isLicensedOrSubscribed(
|
const hasHttpFeature = await isLicensedOrSubscribed(
|
||||||
existingSiteResource.orgId,
|
existingSiteResource.orgId,
|
||||||
tierMatrix[TierFeature.HTTPPrivateResources]
|
tierMatrix[TierFeature.AdvancedPrivateResources]
|
||||||
);
|
);
|
||||||
if (!hasHttpFeature) {
|
if (!hasHttpFeature) {
|
||||||
return next(
|
return next(
|
||||||
@@ -310,7 +328,7 @@ export async function updateSiteResource(
|
|||||||
|
|
||||||
const isLicensedSshPam = await isLicensedOrSubscribed(
|
const isLicensedSshPam = await isLicensedOrSubscribed(
|
||||||
existingSiteResource.orgId,
|
existingSiteResource.orgId,
|
||||||
tierMatrix.sshPam
|
tierMatrix.advancedPrivateResources
|
||||||
);
|
);
|
||||||
|
|
||||||
const [org] = await db
|
const [org] = await db
|
||||||
@@ -632,6 +650,15 @@ export async function updateSiteResource(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
||||||
|
if (mode === "http") {
|
||||||
|
tcpPortRangeStringAdjusted = "443,80";
|
||||||
|
} else if (mode === "ssh") {
|
||||||
|
tcpPortRangeStringAdjusted = destinationPort
|
||||||
|
? destinationPort.toString()
|
||||||
|
: "22";
|
||||||
|
}
|
||||||
|
|
||||||
[updatedSiteResource] = await trx
|
[updatedSiteResource] = await trx
|
||||||
.update(siteResources)
|
.update(siteResources)
|
||||||
.set({
|
.set({
|
||||||
@@ -644,9 +671,14 @@ export async function updateSiteResource(
|
|||||||
destinationPort: destinationPort,
|
destinationPort: destinationPort,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
alias: alias ? alias.trim() : null,
|
alias: alias ? alias.trim() : null,
|
||||||
tcpPortRangeString: tcpPortRangeString,
|
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
||||||
udpPortRangeString: udpPortRangeString,
|
udpPortRangeString:
|
||||||
disableIcmp: disableIcmp,
|
mode == "http" || mode == "ssh"
|
||||||
|
? ""
|
||||||
|
: udpPortRangeString,
|
||||||
|
disableIcmp:
|
||||||
|
disableIcmp ||
|
||||||
|
(mode == "http" || mode == "ssh" ? true : false),
|
||||||
domainId,
|
domainId,
|
||||||
subdomain: finalSubdomain,
|
subdomain: finalSubdomain,
|
||||||
fullDomain,
|
fullDomain,
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ export function generateName(): string {
|
|||||||
return name.replace(/[^a-z0-9-]/g, "");
|
return name.replace(/[^a-z0-9-]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
await migration();
|
|
||||||
|
|
||||||
export default async function migration() {
|
export default async function migration() {
|
||||||
console.log(`Running setup script ${version}...`);
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import {
|
|||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { type Selectedsite } from "@app/components/site-selector";
|
import { type Selectedsite } from "@app/components/site-selector";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -48,13 +51,21 @@ 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 disabled = !isPaidUser(
|
||||||
|
tierMatrix[TierFeature.AdvancedPublicResources]
|
||||||
|
);
|
||||||
|
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
@@ -63,11 +74,13 @@ export default function SshSettingsPage(props: {
|
|||||||
function SshServerForm({
|
function SshServerForm({
|
||||||
orgId,
|
orgId,
|
||||||
resource,
|
resource,
|
||||||
updateResource
|
updateResource,
|
||||||
|
disabled
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resource: GetResourceResponse;
|
resource: GetResourceResponse;
|
||||||
updateResource: ResourceContextType["updateResource"];
|
updateResource: ResourceContextType["updateResource"];
|
||||||
|
disabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
@@ -220,31 +233,36 @@ function SshServerForm({
|
|||||||
{t("rdpServerDescription")}
|
{t("rdpServerDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<fieldset
|
||||||
<SettingsSectionForm variant="half">
|
disabled={disabled}
|
||||||
<BrowserGatewayTargetForm
|
className={disabled ? "opacity-50 pointer-events-none" : ""}
|
||||||
orgId={orgId}
|
>
|
||||||
multiSite={true}
|
<SettingsSectionBody>
|
||||||
selectedSites={selectedSites}
|
<SettingsSectionForm variant="half">
|
||||||
onSitesChange={setSelectedSites}
|
<BrowserGatewayTargetForm
|
||||||
destination={bgDestination}
|
orgId={orgId}
|
||||||
destinationPort={bgDestinationPort}
|
multiSite={true}
|
||||||
onDestinationChange={setBgDestination}
|
selectedSites={selectedSites}
|
||||||
onDestinationPortChange={setBgDestinationPort}
|
onSitesChange={setSelectedSites}
|
||||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
|
destination={bgDestination}
|
||||||
defaultPort={3389}
|
destinationPort={bgDestinationPort}
|
||||||
/>
|
onDestinationChange={setBgDestination}
|
||||||
</SettingsSectionForm>
|
onDestinationPortChange={setBgDestinationPort}
|
||||||
</SettingsSectionBody>
|
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
|
||||||
<form action={formAction} className="flex justify-end mt-4">
|
defaultPort={3389}
|
||||||
<Button
|
/>
|
||||||
disabled={isSubmitting}
|
</SettingsSectionForm>
|
||||||
loading={isSubmitting}
|
</SettingsSectionBody>
|
||||||
type="submit"
|
<form action={formAction} className="flex justify-end mt-4">
|
||||||
>
|
<Button
|
||||||
{t("saveSettings")}
|
disabled={isSubmitting}
|
||||||
</Button>
|
loading={isSubmitting}
|
||||||
</form>
|
type="submit"
|
||||||
|
>
|
||||||
|
{t("saveSettings")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ import {
|
|||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { StrategySelect, StrategyOption } from "@app/components/StrategySelect";
|
import { StrategySelect, StrategyOption } from "@app/components/StrategySelect";
|
||||||
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import {
|
import {
|
||||||
SitesSelector,
|
SitesSelector,
|
||||||
type Selectedsite
|
type Selectedsite
|
||||||
} from "@app/components/site-selector";
|
} from "@app/components/site-selector";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -68,13 +71,21 @@ 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 disabled = !isPaidUser(
|
||||||
|
tierMatrix[TierFeature.AdvancedPublicResources]
|
||||||
|
);
|
||||||
|
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
@@ -83,11 +94,13 @@ export default function SshSettingsPage(props: {
|
|||||||
function SshServerForm({
|
function SshServerForm({
|
||||||
orgId,
|
orgId,
|
||||||
resource,
|
resource,
|
||||||
updateResource
|
updateResource,
|
||||||
|
disabled
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resource: GetResourceResponse;
|
resource: GetResourceResponse;
|
||||||
updateResource: ResourceContextType["updateResource"];
|
updateResource: ResourceContextType["updateResource"];
|
||||||
|
disabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
@@ -366,6 +379,10 @@ function SshServerForm({
|
|||||||
{t("sshServerDescription")}
|
{t("sshServerDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
|
<fieldset
|
||||||
|
disabled={disabled}
|
||||||
|
className={disabled ? "opacity-50 pointer-events-none" : ""}
|
||||||
|
>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm variant="half">
|
<SettingsSectionForm variant="half">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -480,7 +497,8 @@ function SshServerForm({
|
|||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
) : standardDaemonLocation !== "site" ? (
|
) : standardDaemonLocation !== "site" ||
|
||||||
|
pamMode === "passthrough" ? (
|
||||||
<BrowserGatewayTargetForm
|
<BrowserGatewayTargetForm
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
multiSite={true}
|
multiSite={true}
|
||||||
@@ -519,6 +537,7 @@ function SshServerForm({
|
|||||||
{t("saveSettings")}
|
{t("saveSettings")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</fieldset>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import {
|
|||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { type Selectedsite } from "@app/components/site-selector";
|
import { type Selectedsite } from "@app/components/site-selector";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -46,13 +49,21 @@ 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 disabled = !isPaidUser(
|
||||||
|
tierMatrix[TierFeature.AdvancedPublicResources]
|
||||||
|
);
|
||||||
|
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
@@ -61,11 +72,13 @@ export default function SshSettingsPage(props: {
|
|||||||
function SshServerForm({
|
function SshServerForm({
|
||||||
orgId,
|
orgId,
|
||||||
resource,
|
resource,
|
||||||
updateResource
|
updateResource,
|
||||||
|
disabled
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resource: GetResourceResponse;
|
resource: GetResourceResponse;
|
||||||
updateResource: ResourceContextType["updateResource"];
|
updateResource: ResourceContextType["updateResource"];
|
||||||
|
disabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
@@ -218,31 +231,36 @@ function SshServerForm({
|
|||||||
{t("vncServerDescription")}
|
{t("vncServerDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<fieldset
|
||||||
<SettingsSectionForm variant="half">
|
disabled={disabled}
|
||||||
<BrowserGatewayTargetForm
|
className={disabled ? "opacity-50 pointer-events-none" : ""}
|
||||||
orgId={orgId}
|
>
|
||||||
multiSite={true}
|
<SettingsSectionBody>
|
||||||
selectedSites={selectedSites}
|
<SettingsSectionForm variant="half">
|
||||||
onSitesChange={setSelectedSites}
|
<BrowserGatewayTargetForm
|
||||||
destination={bgDestination}
|
orgId={orgId}
|
||||||
destinationPort={bgDestinationPort}
|
multiSite={true}
|
||||||
onDestinationChange={setBgDestination}
|
selectedSites={selectedSites}
|
||||||
onDestinationPortChange={setBgDestinationPort}
|
onSitesChange={setSelectedSites}
|
||||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc"
|
destination={bgDestination}
|
||||||
defaultPort={5900}
|
destinationPort={bgDestinationPort}
|
||||||
/>
|
onDestinationChange={setBgDestination}
|
||||||
</SettingsSectionForm>
|
onDestinationPortChange={setBgDestinationPort}
|
||||||
</SettingsSectionBody>
|
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc"
|
||||||
<form action={formAction} className="flex justify-end mt-4">
|
defaultPort={5900}
|
||||||
<Button
|
/>
|
||||||
disabled={isSubmitting}
|
</SettingsSectionForm>
|
||||||
loading={isSubmitting}
|
</SettingsSectionBody>
|
||||||
type="submit"
|
<form action={formAction} className="flex justify-end mt-4">
|
||||||
>
|
<Button
|
||||||
{t("saveSettings")}
|
disabled={isSubmitting}
|
||||||
</Button>
|
loading={isSubmitting}
|
||||||
</form>
|
type="submit"
|
||||||
|
>
|
||||||
|
{t("saveSettings")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
StrategySelect,
|
StrategySelect,
|
||||||
type StrategyOption
|
type StrategyOption
|
||||||
} from "@app/components/StrategySelect";
|
} from "@app/components/StrategySelect";
|
||||||
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
|
|
||||||
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
||||||
import {
|
import {
|
||||||
SitesSelector,
|
SitesSelector,
|
||||||
@@ -73,7 +72,10 @@ import {
|
|||||||
} from "@app/components/ui/tooltip";
|
} from "@app/components/ui/tooltip";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
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 { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { orgQueries } from "@app/lib/queries";
|
||||||
@@ -227,6 +229,8 @@ export default function Page() {
|
|||||||
orgQueries.sites({ orgId: orgId as string })
|
orgQueries.sites({ orgId: orgId as string })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
const [remoteExitNodes, setRemoteExitNodes] = useState<
|
const [remoteExitNodes, setRemoteExitNodes] = useState<
|
||||||
ListRemoteExitNodesResponse["remoteExitNodes"]
|
ListRemoteExitNodesResponse["remoteExitNodes"]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -239,6 +243,14 @@ 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[]>([]);
|
||||||
|
|
||||||
@@ -871,6 +883,14 @@ 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")}
|
||||||
@@ -879,6 +899,14 @@ 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">
|
||||||
{/* Mode */}
|
{/* Mode */}
|
||||||
@@ -896,26 +924,23 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auth Method (standard only) */}
|
<div className="space-y-3">
|
||||||
{!isNative && (
|
<p className="text-sm font-semibold">
|
||||||
<div className="space-y-3">
|
{t(
|
||||||
<p className="text-sm font-semibold">
|
"sshAuthenticationMethod"
|
||||||
{t(
|
)}
|
||||||
"sshAuthenticationMethod"
|
</p>
|
||||||
)}
|
<StrategySelect<
|
||||||
</p>
|
"passthrough" | "push"
|
||||||
<StrategySelect<
|
>
|
||||||
"passthrough" | "push"
|
value={pamMode}
|
||||||
>
|
options={
|
||||||
value={pamMode}
|
authMethodOptions
|
||||||
options={
|
}
|
||||||
authMethodOptions
|
onChange={setPamMode}
|
||||||
}
|
cols={2}
|
||||||
onChange={setPamMode}
|
/>
|
||||||
cols={2}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Daemon Location (standard + push) */}
|
{/* Daemon Location (standard + push) */}
|
||||||
{showDaemonLocation && (
|
{showDaemonLocation && (
|
||||||
@@ -1046,7 +1071,9 @@ export default function Page() {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
) : standardDaemonLocation !==
|
) : standardDaemonLocation !==
|
||||||
"site" ? (
|
"site" ||
|
||||||
|
pamMode ===
|
||||||
|
"passthrough" ? (
|
||||||
<BrowserGatewayTargetForm
|
<BrowserGatewayTargetForm
|
||||||
orgId={orgId as string}
|
orgId={orgId as string}
|
||||||
multiSite={true}
|
multiSite={true}
|
||||||
@@ -1100,12 +1127,21 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</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")}
|
||||||
@@ -1114,6 +1150,14 @@ 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">
|
||||||
<BrowserGatewayTargetForm
|
<BrowserGatewayTargetForm
|
||||||
@@ -1138,12 +1182,21 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
</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")}
|
||||||
@@ -1152,6 +1205,14 @@ 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">
|
||||||
<BrowserGatewayTargetForm
|
<BrowserGatewayTargetForm
|
||||||
@@ -1176,6 +1237,7 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
|
</fieldset>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1227,7 +1289,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
disabled={!areAllTargetsValid()}
|
disabled={!areAllTargetsValid() || browserGatewayDisabled}
|
||||||
>
|
>
|
||||||
{t("resourceCreate")}
|
{t("resourceCreate")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export default function CreatePrivateResourceDialog({
|
|||||||
data.alias.trim()
|
data.alias.trim()
|
||||||
? data.alias
|
? data.alias
|
||||||
: undefined,
|
: undefined,
|
||||||
|
destinationPort: data.destinationPort ?? undefined,
|
||||||
pamMode: data.pamMode ?? undefined,
|
pamMode: data.pamMode ?? undefined,
|
||||||
...(data.authDaemonMode != null && {
|
...(data.authDaemonMode != null && {
|
||||||
authDaemonMode: data.authDaemonMode
|
authDaemonMode: data.authDaemonMode
|
||||||
@@ -112,13 +113,14 @@ export default function CreatePrivateResourceDialog({
|
|||||||
authDaemonPort: data.authDaemonPort
|
authDaemonPort: data.authDaemonPort
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
...((data.mode === "host" ||
|
...((data.mode === "host" || data.mode === "cidr") && {
|
||||||
data.mode === "ssh" ||
|
|
||||||
data.mode === "cidr") && {
|
|
||||||
tcpPortRangeString: data.tcpPortRangeString,
|
tcpPortRangeString: data.tcpPortRangeString,
|
||||||
udpPortRangeString: data.udpPortRangeString,
|
udpPortRangeString: data.udpPortRangeString,
|
||||||
disableIcmp: data.disableIcmp ?? false
|
disableIcmp: data.disableIcmp ?? false
|
||||||
}),
|
}),
|
||||||
|
...(data.mode === "ssh" && {
|
||||||
|
disableIcmp: data.disableIcmp ?? false
|
||||||
|
}),
|
||||||
roleIds: data.roles
|
roleIds: data.roles
|
||||||
? data.roles.map((r) => parseInt(r.id))
|
? data.roles.map((r) => parseInt(r.id))
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
|
|||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
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 type {
|
import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
||||||
CreateRoleBody,
|
|
||||||
CreateRoleResponse
|
|
||||||
} from "@server/routers/role";
|
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
@@ -50,7 +47,7 @@ export default function CreateRoleForm({
|
|||||||
requireDeviceApproval: values.requireDeviceApproval,
|
requireDeviceApproval: values.requireDeviceApproval,
|
||||||
allowSsh: values.allowSsh
|
allowSsh: values.allowSsh
|
||||||
};
|
};
|
||||||
if (isPaidUser(tierMatrix.sshPam)) {
|
if (isPaidUser(tierMatrix.advancedPrivateResources)) {
|
||||||
payload.sshSudoMode = values.sshSudoMode;
|
payload.sshSudoMode = values.sshSudoMode;
|
||||||
payload.sshCreateHomeDir = values.sshCreateHomeDir;
|
payload.sshCreateHomeDir = values.sshCreateHomeDir;
|
||||||
payload.sshSudoCommands =
|
payload.sshSudoCommands =
|
||||||
@@ -69,10 +66,9 @@ export default function CreateRoleForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await api
|
const res = await api
|
||||||
.put<AxiosResponse<CreateRoleResponse>>(
|
.put<
|
||||||
`/org/${org?.org.orgId}/role`,
|
AxiosResponse<CreateRoleResponse>
|
||||||
payload
|
>(`/org/${org?.org.orgId}/role`, payload)
|
||||||
)
|
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export default function EditPrivateResourceDialog({
|
|||||||
data.alias.trim()
|
data.alias.trim()
|
||||||
? data.alias
|
? data.alias
|
||||||
: null,
|
: null,
|
||||||
|
destinationPort: data.destinationPort ?? null,
|
||||||
pamMode: data.pamMode ?? undefined,
|
pamMode: data.pamMode ?? undefined,
|
||||||
...(data.authDaemonMode != null && {
|
...(data.authDaemonMode != null && {
|
||||||
authDaemonMode: data.authDaemonMode
|
authDaemonMode: data.authDaemonMode
|
||||||
@@ -112,13 +113,14 @@ export default function EditPrivateResourceDialog({
|
|||||||
authDaemonPort: data.authDaemonPort || null
|
authDaemonPort: data.authDaemonPort || null
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
...((data.mode === "host" ||
|
...((data.mode === "host" || data.mode === "cidr") && {
|
||||||
data.mode === "ssh" ||
|
|
||||||
data.mode === "cidr") && {
|
|
||||||
tcpPortRangeString: data.tcpPortRangeString,
|
tcpPortRangeString: data.tcpPortRangeString,
|
||||||
udpPortRangeString: data.udpPortRangeString,
|
udpPortRangeString: data.udpPortRangeString,
|
||||||
disableIcmp: data.disableIcmp ?? false
|
disableIcmp: data.disableIcmp ?? false
|
||||||
}),
|
}),
|
||||||
|
...(data.mode === "ssh" && {
|
||||||
|
disableIcmp: data.disableIcmp ?? false
|
||||||
|
}),
|
||||||
roleIds: (data.roles || []).map((r) => parseInt(r.id)),
|
roleIds: (data.roles || []).map((r) => parseInt(r.id)),
|
||||||
userIds: (data.users || []).map((u) => u.id),
|
userIds: (data.users || []).map((u) => u.id),
|
||||||
clientIds: (data.clients || []).map((c) => parseInt(c.id))
|
clientIds: (data.clients || []).map((c) => parseInt(c.id))
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ 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 type { Role } from "@server/db";
|
import type { Role } from "@server/db";
|
||||||
import type {
|
import type { UpdateRoleBody, UpdateRoleResponse } from "@server/routers/role";
|
||||||
UpdateRoleBody,
|
|
||||||
UpdateRoleResponse
|
|
||||||
} from "@server/routers/role";
|
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
@@ -53,7 +50,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.sshPam)) {
|
if (isPaidUser(tierMatrix.advancedPrivateResources)) {
|
||||||
payload.sshSudoMode = values.sshSudoMode;
|
payload.sshSudoMode = values.sshSudoMode;
|
||||||
payload.sshCreateHomeDir = values.sshCreateHomeDir;
|
payload.sshCreateHomeDir = values.sshCreateHomeDir;
|
||||||
payload.sshSudoCommands =
|
payload.sshSudoCommands =
|
||||||
@@ -72,10 +69,9 @@ export default function EditRoleForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await api
|
const res = await api
|
||||||
.post<AxiosResponse<UpdateRoleResponse>>(
|
.post<
|
||||||
`/role/${role.roleId}`,
|
AxiosResponse<UpdateRoleResponse>
|
||||||
payload
|
>(`/role/${role.roleId}`, payload)
|
||||||
)
|
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
// Update Resource type to include site information
|
// Update Resource type to include site information
|
||||||
type Resource = {
|
type Resource = {
|
||||||
@@ -49,7 +50,7 @@ type Resource = {
|
|||||||
domain: string;
|
domain: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
protected: boolean;
|
protected: boolean;
|
||||||
// mode: string; // "http", "tcp", "udp", "rdp", "vnc", "ssh"
|
mode: string; // "http", "tcp", "udp", "rdp", "vnc", "ssh"
|
||||||
// Auth method fields
|
// Auth method fields
|
||||||
sso?: boolean;
|
sso?: boolean;
|
||||||
password?: boolean;
|
password?: boolean;
|
||||||
@@ -62,6 +63,7 @@ type Resource = {
|
|||||||
type SiteResource = {
|
type SiteResource = {
|
||||||
siteResourceId: number;
|
siteResourceId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
niceId: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
ssl: boolean;
|
ssl: boolean;
|
||||||
@@ -754,7 +756,13 @@ export default function MemberResourcesPortal({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{resource.mode.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
<ResourceInfo
|
<ResourceInfo
|
||||||
resource={resource}
|
resource={resource}
|
||||||
/>
|
/>
|
||||||
@@ -860,7 +868,13 @@ export default function MemberResourcesPortal({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{siteResource.mode.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
<InfoPopup>
|
<InfoPopup>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="text-xs font-medium mb-1.5">
|
<div className="text-xs font-medium mb-1.5">
|
||||||
@@ -876,24 +890,24 @@ export default function MemberResourcesPortal({
|
|||||||
:
|
:
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground capitalize">
|
<span className="ml-2 text-muted-foreground capitalize">
|
||||||
{
|
{siteResource.mode.toUpperCase()}
|
||||||
siteResource.mode
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">
|
|
||||||
{t(
|
|
||||||
"memberPortalDestination"
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
<span className="ml-2 text-muted-foreground">
|
|
||||||
{
|
|
||||||
siteResource.destination
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{siteResource.destination && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">
|
||||||
|
{t(
|
||||||
|
"memberPortalDestination"
|
||||||
|
)}
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 text-muted-foreground">
|
||||||
|
{
|
||||||
|
siteResource.destination
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{siteResource.alias && (
|
{siteResource.alias && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
@@ -942,45 +956,35 @@ export default function MemberResourcesPortal({
|
|||||||
isLink={true}
|
isLink={true}
|
||||||
/>
|
/>
|
||||||
) : siteResource.alias ? (
|
) : siteResource.alias ? (
|
||||||
<>
|
/* Alias as primary */
|
||||||
{/* Alias as primary */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="text-sm text-muted-foreground font-medium text-left truncate flex-1">
|
||||||
<div className="text-base font-semibold text-foreground text-left truncate flex-1">
|
{siteResource.alias}
|
||||||
{
|
</div>
|
||||||
siteResource.alias
|
<Button
|
||||||
}
|
variant="ghost"
|
||||||
</div>
|
size="icon"
|
||||||
<Button
|
className="h-8 w-8 text-muted-foreground"
|
||||||
variant="ghost"
|
onClick={() => {
|
||||||
size="icon"
|
navigator.clipboard.writeText(
|
||||||
className="h-8 w-8 text-muted-foreground"
|
siteResource.alias!
|
||||||
onClick={() => {
|
);
|
||||||
navigator.clipboard.writeText(
|
toast({
|
||||||
siteResource.alias!
|
title: t(
|
||||||
);
|
"memberPortalCopiedToClipboard"
|
||||||
toast({
|
),
|
||||||
title: t(
|
description:
|
||||||
"memberPortalCopiedToClipboard"
|
t(
|
||||||
|
"memberPortalCopiedAliasDescription"
|
||||||
),
|
),
|
||||||
description:
|
duration: 2000
|
||||||
t(
|
});
|
||||||
"memberPortalCopiedAliasDescription"
|
}}
|
||||||
),
|
>
|
||||||
duration: 2000
|
<Copy className="h-4 w-4" />
|
||||||
});
|
</Button>
|
||||||
}}
|
</div>
|
||||||
>
|
) : siteResource.destination ? (
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/* Destination as secondary */}
|
|
||||||
<div className="text-xs text-muted-foreground truncate">
|
|
||||||
{
|
|
||||||
siteResource.destination
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
/* Destination as primary when no alias */
|
/* Destination as primary when no alias */
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-sm text-muted-foreground font-medium text-left truncate flex-1">
|
<div className="text-sm text-muted-foreground font-medium text-left truncate flex-1">
|
||||||
@@ -1011,6 +1015,37 @@ export default function MemberResourcesPortal({
|
|||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* niceId fallback when no alias and no destination */
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-sm text-muted-foreground font-medium text-left truncate flex-1">
|
||||||
|
{
|
||||||
|
siteResource.niceId
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
siteResource.niceId
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: t(
|
||||||
|
"memberPortalCopiedToClipboard"
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
t(
|
||||||
|
"memberPortalCopiedDestinationDescription"
|
||||||
|
),
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,8 +224,10 @@ export function PrivateResourceForm({
|
|||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const disableEnterpriseFeatures = env.flags.disableEnterpriseFeatures;
|
const disableEnterpriseFeatures = env.flags.disableEnterpriseFeatures;
|
||||||
const sshSectionDisabled = !isPaidUser(tierMatrix.sshPam);
|
const sshSectionDisabled = !isPaidUser(tierMatrix.advancedPrivateResources);
|
||||||
const httpSectionDisabled = !isPaidUser(tierMatrix.httpPrivateResources);
|
const httpSectionDisabled = !isPaidUser(
|
||||||
|
tierMatrix.advancedPrivateResources
|
||||||
|
);
|
||||||
|
|
||||||
const nameRequiredKey =
|
const nameRequiredKey =
|
||||||
variant === "create"
|
variant === "create"
|
||||||
@@ -365,6 +367,19 @@ export function PrivateResourceForm({
|
|||||||
path: ["destination"]
|
path: ["destination"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (data.mode === "ssh" && !isNativeSsh) {
|
||||||
|
if (
|
||||||
|
data.destinationPort == null ||
|
||||||
|
!Number.isFinite(data.destinationPort) ||
|
||||||
|
data.destinationPort < 1
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("internalResourceHttpPortRequired"),
|
||||||
|
path: ["destinationPort"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
if (data.mode !== "http") return;
|
if (data.mode !== "http") return;
|
||||||
if (!data.scheme) {
|
if (!data.scheme) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
@@ -548,7 +563,7 @@ export function PrivateResourceForm({
|
|||||||
mode: "host",
|
mode: "host",
|
||||||
destination: "",
|
destination: "",
|
||||||
alias: null,
|
alias: null,
|
||||||
destinationPort: null,
|
destinationPort: 22,
|
||||||
scheme: "http",
|
scheme: "http",
|
||||||
ssl: true,
|
ssl: true,
|
||||||
httpConfigSubdomain: null,
|
httpConfigSubdomain: null,
|
||||||
@@ -581,6 +596,7 @@ export function PrivateResourceForm({
|
|||||||
const httpConfigDomainId = form.watch("httpConfigDomainId");
|
const httpConfigDomainId = form.watch("httpConfigDomainId");
|
||||||
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
|
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
|
||||||
const isHttpMode = mode === "http";
|
const isHttpMode = mode === "http";
|
||||||
|
const isSshMode = mode === "ssh";
|
||||||
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
|
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
|
||||||
const pamMode = form.watch("pamMode") ?? "passthrough";
|
const pamMode = form.watch("pamMode") ?? "passthrough";
|
||||||
const isNative = sshServerMode === "native";
|
const isNative = sshServerMode === "native";
|
||||||
@@ -726,8 +742,17 @@ export function PrivateResourceForm({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onSubmitDisabledChange?.(isHttpMode && httpSectionDisabled);
|
onSubmitDisabledChange?.(
|
||||||
}, [isHttpMode, httpSectionDisabled, onSubmitDisabledChange]);
|
(isHttpMode && httpSectionDisabled) ||
|
||||||
|
(isSshMode && sshSectionDisabled)
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
isHttpMode,
|
||||||
|
httpSectionDisabled,
|
||||||
|
isSshMode,
|
||||||
|
sshSectionDisabled,
|
||||||
|
onSubmitDisabledChange
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -735,6 +760,7 @@ export function PrivateResourceForm({
|
|||||||
onSubmit={form.handleSubmit((values) => {
|
onSubmit={form.handleSubmit((values) => {
|
||||||
const siteIds = values.siteIds;
|
const siteIds = values.siteIds;
|
||||||
const trimmedDestination = values.destination?.trim();
|
const trimmedDestination = values.destination?.trim();
|
||||||
|
const isSshMode = values.mode === "ssh";
|
||||||
onSubmit({
|
onSubmit({
|
||||||
...values,
|
...values,
|
||||||
siteIds,
|
siteIds,
|
||||||
@@ -742,6 +768,12 @@ export function PrivateResourceForm({
|
|||||||
trimmedDestination && trimmedDestination.length > 0
|
trimmedDestination && trimmedDestination.length > 0
|
||||||
? trimmedDestination
|
? trimmedDestination
|
||||||
: null,
|
: null,
|
||||||
|
tcpPortRangeString: isSshMode
|
||||||
|
? undefined
|
||||||
|
: values.tcpPortRangeString,
|
||||||
|
udpPortRangeString: isSshMode
|
||||||
|
? undefined
|
||||||
|
: values.udpPortRangeString,
|
||||||
clients: (values.clients ?? []).map((c) => ({
|
clients: (values.clients ?? []).map((c) => ({
|
||||||
id: c.clientId.toString(),
|
id: c.clientId.toString(),
|
||||||
text: c.name
|
text: c.name
|
||||||
@@ -826,8 +858,11 @@ export function PrivateResourceForm({
|
|||||||
{t("sites")}
|
{t("sites")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
{mode === "ssh" &&
|
{mode === "ssh" &&
|
||||||
sshServerMode ===
|
(sshServerMode ===
|
||||||
"native" ? (
|
"native" ||
|
||||||
|
(pamMode === "push" &&
|
||||||
|
authDaemonMode ===
|
||||||
|
"site")) ? (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
asChild
|
asChild
|
||||||
@@ -1106,8 +1141,10 @@ export function PrivateResourceForm({
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
isHttpMode &&
|
(isHttpMode &&
|
||||||
httpSectionDisabled
|
httpSectionDisabled) ||
|
||||||
|
(isSshMode &&
|
||||||
|
sshSectionDisabled)
|
||||||
}
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
field.onChange(
|
field.onChange(
|
||||||
@@ -1146,6 +1183,10 @@ export function PrivateResourceForm({
|
|||||||
field.value ??
|
field.value ??
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
disabled={
|
||||||
|
isSshMode &&
|
||||||
|
sshSectionDisabled
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -1179,7 +1220,10 @@ export function PrivateResourceForm({
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
httpSectionDisabled
|
(isHttpMode &&
|
||||||
|
httpSectionDisabled) ||
|
||||||
|
(isSshMode &&
|
||||||
|
sshSectionDisabled)
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const raw =
|
const raw =
|
||||||
@@ -1214,9 +1258,9 @@ export function PrivateResourceForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isHttpMode && (
|
{(isHttpMode || isSshMode) && (
|
||||||
<PaidFeaturesAlert
|
<PaidFeaturesAlert
|
||||||
tiers={tierMatrix.httpPrivateResources}
|
tiers={tierMatrix.advancedPrivateResources}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1750,7 +1794,9 @@ export function PrivateResourceForm({
|
|||||||
{/* SSH Access tab (ssh mode only) */}
|
{/* SSH Access tab (ssh mode only) */}
|
||||||
{!disableEnterpriseFeatures && mode === "ssh" && (
|
{!disableEnterpriseFeatures && mode === "ssh" && (
|
||||||
<div className="space-y-4 mt-4 p-1">
|
<div className="space-y-4 mt-4 p-1">
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
|
<PaidFeaturesAlert
|
||||||
|
tiers={tierMatrix.advancedPrivateResources}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Mode */}
|
{/* Mode */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -1862,6 +1908,36 @@ export function PrivateResourceForm({
|
|||||||
"authDaemonPort",
|
"authDaemonPort",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
} else if (
|
||||||
|
v === "push"
|
||||||
|
) {
|
||||||
|
// push + site (default) = single site
|
||||||
|
const curAuthMode =
|
||||||
|
form.getValues(
|
||||||
|
"authDaemonMode"
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
curAuthMode !==
|
||||||
|
"remote" &&
|
||||||
|
selectedSites.length >
|
||||||
|
1
|
||||||
|
) {
|
||||||
|
const first =
|
||||||
|
selectedSites.slice(
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
setSelectedSites(
|
||||||
|
first
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"siteIds",
|
||||||
|
first.map(
|
||||||
|
(s) =>
|
||||||
|
s.siteId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
cols={2}
|
cols={2}
|
||||||
@@ -1929,6 +2005,29 @@ export function PrivateResourceForm({
|
|||||||
"authDaemonPort",
|
"authDaemonPort",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
// site daemon = single site
|
||||||
|
if (
|
||||||
|
selectedSites.length >
|
||||||
|
1
|
||||||
|
) {
|
||||||
|
const first =
|
||||||
|
selectedSites.slice(
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
setSelectedSites(
|
||||||
|
first
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"siteIds",
|
||||||
|
first.map(
|
||||||
|
(
|
||||||
|
s
|
||||||
|
) =>
|
||||||
|
s.siteId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
cols={2}
|
cols={2}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export function RoleForm({
|
|||||||
}
|
}
|
||||||
}, [variant, role, form]);
|
}, [variant, role, form]);
|
||||||
|
|
||||||
const sshDisabled = !isPaidUser(tierMatrix.sshPam);
|
const sshDisabled = !isPaidUser(tierMatrix.advancedPrivateResources);
|
||||||
const sshSudoMode = form.watch("sshSudoMode");
|
const sshSudoMode = form.watch("sshSudoMode");
|
||||||
const isAdminRole = variant === "edit" && role?.isAdmin === true;
|
const isAdminRole = variant === "edit" && role?.isAdmin === true;
|
||||||
|
|
||||||
@@ -319,7 +319,9 @@ export function RoleForm({
|
|||||||
{/* SSH tab - hidden when enterprise features are disabled */}
|
{/* SSH tab - hidden when enterprise features are disabled */}
|
||||||
{!env.flags.disableEnterpriseFeatures && (
|
{!env.flags.disableEnterpriseFeatures && (
|
||||||
<div className="space-y-4 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
|
<PaidFeaturesAlert
|
||||||
|
tiers={tierMatrix.advancedPrivateResources}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="allowSsh"
|
name="allowSsh"
|
||||||
|
|||||||
Reference in New Issue
Block a user