Compare commits

...

4 Commits

Author SHA1 Message Date
miloschwartz bf79768e05 update links 2026-05-31 20:20:37 -07:00
Owen 08a2923cfc Move proxy and client to public and private 2026-05-31 17:30:31 -07:00
Owen b99e9a6468 Working on ui 2026-05-31 17:25:03 -07:00
Owen cb2ee9c489 Fixing visual issues 2026-05-31 16:36:13 -07:00
39 changed files with 268 additions and 206 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ the docs to illustrate some basic ideas.
## Licensing
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
## Contributions
+15 -1
View File
@@ -22,7 +22,21 @@ const nextConfig: NextConfig = {
reactCompiler: true,
transpilePackages: ["@novnc/novnc"],
output: "standalone",
allowedDevOrigins
allowedDevOrigins,
async redirects() {
return [
{
source: "/:orgId/settings/resources/proxy/:path*",
destination: "/:orgId/settings/resources/public/:path*",
permanent: true
},
{
source: "/:orgId/settings/resources/client/:path*",
destination: "/:orgId/settings/resources/private/:path*",
permanent: true
}
];
}
};
export default withNextIntl(nextConfig);
@@ -291,6 +291,12 @@ export async function getTraefikConfig(
domainCertResolver: domains.certResolver,
preferWildcardCert: domains.preferWildcardCert,
domainNamespaceId: domainNamespaces.domainNamespaceId,
// Maintenance fields
maintenanceModeEnabled: resources.maintenanceModeEnabled,
maintenanceModeType: resources.maintenanceModeType,
maintenanceTitle: resources.maintenanceTitle,
maintenanceMessage: resources.maintenanceMessage,
maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
// Browser gateway target fields
browserGatewayTargetId: browserGatewayTarget.browserGatewayTargetId,
bgType: browserGatewayTarget.type,
@@ -340,6 +346,11 @@ export async function getTraefikConfig(
wildcard: boolean | null;
domainCertResolver: string | null;
preferWildcardCert: boolean | null;
maintenanceModeEnabled: boolean | null;
maintenanceModeType: string | null;
maintenanceTitle: string | null;
maintenanceMessage: string | null;
maintenanceEstimatedTime: string | null;
targets: {
browserGatewayTargetId: number;
bgType: string;
@@ -371,6 +382,11 @@ export async function getTraefikConfig(
wildcard: row.wildcard,
domainCertResolver: row.domainCertResolver,
preferWildcardCert: row.preferWildcardCert,
maintenanceModeEnabled: row.maintenanceModeEnabled,
maintenanceModeType: row.maintenanceModeType,
maintenanceTitle: row.maintenanceTitle,
maintenanceMessage: row.maintenanceMessage,
maintenanceEstimatedTime: row.maintenanceEstimatedTime,
targets: []
});
}
@@ -1118,6 +1134,75 @@ export async function getTraefikConfig(
// Collect online sites for this resource (for any type)
const anySiteOnline = bgResource.targets.some((t) => t.siteOnline);
// Maintenance page logic for browser gateway resources
let showBgMaintenancePage = false;
if (bgResource.maintenanceModeEnabled) {
if (bgResource.maintenanceModeType === "forced") {
showBgMaintenancePage = true;
} else if (bgResource.maintenanceModeType === "automatic") {
showBgMaintenancePage = !anySiteOnline;
}
}
if (showBgMaintenancePage && allowMaintenancePage) {
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint;
const entrypointHttps =
config.getRawConfig().traefik.https_entrypoint;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
if (!config_output.http.services) config_output.http.services = {};
if (!config_output.http.middlewares)
config_output.http.middlewares = {};
if (!config_output.http.routers) config_output.http.routers = {};
config_output.http.services![bgMaintenanceServiceName] = {
loadBalancer: {
servers: [
{ url: `http://${maintenanceHost}:${maintenancePort}` }
],
passHostHeader: true
}
};
config_output.http.middlewares![bgRewriteMiddlewareName] = {
replacePathRegex: {
regex: "^/(.*)",
replacement: "/maintenance-screen"
}
};
config_output.http.routers![bgMaintenanceRouterName] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
middlewares: [bgRewriteMiddlewareName],
rule: hostRule,
priority: 2000,
...(bgResource.ssl ? { tls } : {})
};
config_output.http.routers![`${bgMaintenanceRouterName}-assets`] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 2001,
...(bgResource.ssl ? { tls } : {})
};
continue;
}
// Group targets by type and generate per-type websocket routers and services
const typeMap = new Map<string, typeof bgResource.targets>();
for (const t of bgResource.targets) {
+10 -1
View File
@@ -434,7 +434,16 @@ export async function listResources(
.from(targets)
.innerJoin(sites, eq(targets.siteId, sites.siteId))
.where(and(eq(sites.orgId, orgId), eq(sites.siteId, siteId)));
conditions.push(inArray(resources.resourceId, resourcesWithSite));
const resourcesWithBrowserGateway = db
.select({ resourceId: browserGatewayTarget.resourceId })
.from(browserGatewayTarget)
.where(eq(browserGatewayTarget.siteId, siteId));
conditions.push(
or(
inArray(resources.resourceId, resourcesWithSite),
inArray(resources.resourceId, resourcesWithBrowserGateway)
)
);
}
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
+5 -1
View File
@@ -12,6 +12,7 @@ import {
userSites,
labels,
siteLabels,
browserGatewayTarget,
type Label
} from "@server/db";
import cache from "#dynamic/lib/cache";
@@ -240,6 +241,10 @@ function querySitesBase() {
ON ${siteResources.networkId} = ${siteNetworks.networkId}
WHERE ${siteNetworks.siteId} = ${sites.siteId}
AND ${siteResources.orgId} = ${sites.orgId}
) + (
SELECT COUNT(DISTINCT ${browserGatewayTarget.resourceId})
FROM ${browserGatewayTarget}
WHERE ${browserGatewayTarget.siteId} = ${sites.siteId}
)`,
status: sites.status
})
@@ -307,7 +312,6 @@ export async function listSites(
)
);
}
const parsedParams = listSitesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
@@ -144,14 +144,13 @@ const updateSiteResourceSchema = z
data.destinationPort <= 65535
);
} else if (data.mode === "ssh") {
// just check the destinationPort
// destinationPort is optional for native mode; allow null/undefined
return (
data.destinationPort === undefined ||
(data.destinationPort !== null &&
data.destinationPort >= 1 &&
data.destinationPort <= 65535)
data.destinationPort == null ||
(data.destinationPort >= 1 && data.destinationPort <= 65535)
);
}
return true;
},
{
message:
@@ -343,8 +343,8 @@ export default function GeneralPage() {
<Link
href={
row.original.type === "ssh"
? `/${row.original.orgId}/settings/resources/client?query=${row.original.resourceNiceId}`
: `/${row.original.orgId}/settings/resources/proxy/${row.original.resourceNiceId}`
? `/${row.original.orgId}/settings/resources/private?query=${row.original.resourceNiceId}`
: `/${row.original.orgId}/settings/resources/public/${row.original.resourceNiceId}`
}
>
<Button variant="outline" size="sm">
@@ -325,7 +325,7 @@ export default function ConnectionLogsPage() {
if (row.original.resourceName && row.original.resourceNiceId) {
return (
<Link
href={`/${row.original.orgId}/settings/resources/client/?query=${row.original.resourceNiceId}`}
href={`/${row.original.orgId}/settings/resources/private/?query=${row.original.resourceNiceId}`}
>
<Button variant="outline" size="sm">
{row.original.resourceName}
@@ -400,8 +400,8 @@ export default function GeneralPage() {
<Link
href={
row.original.reason == 108 // for now the client will only have reason 108 so we know where to go
? `/${row.original.orgId}/settings/resources/client?query=${row.original.resourceNiceId}`
: `/${row.original.orgId}/settings/resources/proxy/${row.original.resourceNiceId}`
? `/${row.original.orgId}/settings/resources/private?query=${row.original.resourceNiceId}`
: `/${row.original.orgId}/settings/resources/public/${row.original.resourceNiceId}`
}
onClick={(e) => e.stopPropagation()}
>
+1 -1
View File
@@ -11,5 +11,5 @@ export interface ResourcesPageProps {
export default async function ResourcesPage(props: ResourcesPageProps) {
const params = await props.params;
redirect(`/${params.orgId}/settings/resources/proxy`);
redirect(`/${params.orgId}/settings/resources/public`);
}
@@ -1,5 +1,5 @@
import type { InternalResourceRow } from "@app/components/ClientResourcesTable";
import ClientResourcesTable from "@app/components/ClientResourcesTable";
import type { InternalResourceRow } from "@app/components/PrivateResourcesTable";
import PrivateResourcesTable from "@app/components/PrivateResourcesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import PrivateResourcesBanner from "@app/components/PrivateResourcesBanner";
import { internal } from "@app/lib/api";
@@ -148,7 +148,7 @@ export default async function ClientResourcesPage(
<PrivateResourcesBanner orgId={params.orgId} />
<OrgProvider org={org}>
<ClientResourcesTable
<PrivateResourcesTable
internalResources={internalResourceRows}
orgId={params.orgId}
rowCount={pagination.total}
@@ -548,7 +548,7 @@ export default function GeneralForm() {
if (data.niceId && data.niceId !== resource?.niceId) {
router.replace(
`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`
`/${updated.orgId}/settings/resources/public/${data.niceId}/general`
);
}
@@ -39,69 +39,27 @@ import {
FormLabel,
FormMessage
} from "@app/components/ui/form";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@app/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
import type { ResourceContextType } from "@app/contexts/resourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { DockerManager, DockerState } from "@app/lib/docker";
import { orgQueries, resourceQueries } from "@app/lib/queries";
import { resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build";
import { tlsNameSchema } from "@server/lib/schemas";
import { type GetResourceResponse } from "@server/routers/resource";
import type { ListSitesResponse } from "@server/routers/site";
import { CreateTargetResponse } from "@server/routers/target";
import { ListTargetsResponse } from "@server/routers/target/listTargets";
import { ArrayElement } from "@server/types/ArrayElement";
import { useQuery } from "@tanstack/react-query";
import {
LocalTarget,
ProxyResourceTargetsForm
} from "@app/app/[orgId]/settings/resources/proxy/ProxyResourceTargetsForm";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from "@tanstack/react-table";
import { AxiosResponse } from "axios";
} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm";
import {
AlertTriangle,
CircleCheck,
CircleX,
ExternalLink,
Info,
Plus,
Settings
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import {
use,
useActionState,
useCallback,
useEffect,
useMemo,
useState
} from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -83,22 +83,22 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
const navItems = [
{
title: t("general"),
href: `/{orgId}/settings/resources/proxy/{niceId}/general`
href: `/{orgId}/settings/resources/public/{niceId}/general`
},
{
title: t(`${resource.mode}Settings`),
href: `/{orgId}/settings/resources/proxy/{niceId}/${resource.mode}`
href: `/{orgId}/settings/resources/public/{niceId}/${resource.mode}`
}
];
if (["http", "ssh", "rdp", "vnc"].includes(resource.mode)) {
navItems.push({
title: t("authentication"),
href: `/{orgId}/settings/resources/proxy/{niceId}/authentication`
href: `/{orgId}/settings/resources/public/{niceId}/authentication`
});
// navItems.push({
// title: t("rules"),
// href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
// href: `/{orgId}/settings/resources/public/{niceId}/rules`
// });
}
@@ -10,6 +10,6 @@ export default async function ResourcePage(props: {
}) {
const params = await props.params;
redirect(
`/${params.orgId}/settings/resources/proxy/${params.niceId}/general`
`/${params.orgId}/settings/resources/public/${params.niceId}/general`
);
}
@@ -89,7 +89,7 @@ import { useQuery } from "@tanstack/react-query";
import {
LocalTarget,
ProxyResourceTargetsForm
} from "@app/app/[orgId]/settings/resources/proxy/ProxyResourceTargetsForm";
} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm";
import {
ColumnDef,
flexRender,
@@ -514,7 +514,7 @@ export default function Page() {
}
}
router.push(
`/${orgId}/settings/resources/proxy/${newNiceId}`
`/${orgId}/settings/resources/public/${newNiceId}`
);
} else if (resourceType === "ssh") {
if (isNative) {
@@ -550,7 +550,7 @@ export default function Page() {
}
router.push(
`/${orgId}/settings/resources/proxy/${newNiceId}`
`/${orgId}/settings/resources/public/${newNiceId}`
);
} else if (resourceType === "rdp" || resourceType === "vnc") {
for (const site of bgSelectedSites) {
@@ -566,7 +566,7 @@ export default function Page() {
}
router.push(
`/${orgId}/settings/resources/proxy/${newNiceId}`
`/${orgId}/settings/resources/public/${newNiceId}`
);
} else {
// TCP / UDP — create targets then show snippets
@@ -1308,7 +1308,7 @@ export default function Page() {
type="button"
onClick={() =>
router.push(
`/${orgId}/settings/resources/proxy/${niceId}`
`/${orgId}/settings/resources/public/${niceId}`
)
}
>
+2 -2
View File
@@ -109,7 +109,7 @@ export default async function Page(props: {
{t.rich("loginLegalDisclaimer", {
termsOfService: (chunks) => (
<Link
href="https://pangolin.net/terms-of-service.html"
href="https://pangolin.net/tos"
target="_blank"
rel="noopener noreferrer"
className="underline"
@@ -119,7 +119,7 @@ export default async function Page(props: {
),
privacyPolicy: (chunks) => (
<Link
href="https://pangolin.net/privacy-policy.html"
href="https://pangolin.net/privacy"
target="_blank"
rel="noopener noreferrer"
className="underline"
+2 -2
View File
@@ -71,12 +71,12 @@ export const orgNavSections = (
items: [
{
title: "sidebarProxyResources",
href: "/{orgId}/settings/resources/proxy",
href: "/{orgId}/settings/resources/public",
icon: <Globe className="size-4 flex-none" />
},
{
title: "sidebarClientResources",
href: "/{orgId}/settings/resources/client",
href: "/{orgId}/settings/resources/private",
icon: <GlobeLock className="size-4 flex-none" />
}
]
+2 -2
View File
@@ -91,7 +91,7 @@ export default async function AuthFooter() {
<>
<Separator orientation="vertical" />
<a
href="https://pangolin.net/terms-of-service.html"
href="https://pangolin.net/tos"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
@@ -101,7 +101,7 @@ export default async function AuthFooter() {
</a>
<Separator orientation="vertical" />
<a
href="https://pangolin.net/privacy-policy.html"
href="https://pangolin.net/privacy"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
+1 -1
View File
@@ -130,7 +130,7 @@ const DockerContainersTable: FC<{
useState(true);
const [hideStoppedContainers, setHideStoppedContainers] = useState(false);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
labels: false
labels: true
});
const t = useTranslations();
+9 -9
View File
@@ -422,7 +422,7 @@ export default function GenerateLicenseKeyForm({
{part}
{index === 0 && (
<a
href="https://pangolin.net/fcl.html"
href="https://pangolin.net/fcl"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -601,7 +601,7 @@ export default function GenerateLicenseKeyForm({
"signUpTerms.IAgreeToThe"
)}{" "}
<a
href="https://pangolin.net/terms-of-service.html"
href="https://pangolin.net/tos"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -614,7 +614,7 @@ export default function GenerateLicenseKeyForm({
"signUpTerms.and"
)}{" "}
<a
href="https://pangolin.net/privacy-policy.html"
href="https://pangolin.net/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -658,12 +658,12 @@ export default function GenerateLicenseKeyForm({
license
details:{" "}
<a
href="https://pangolin.net/fcl.html"
href="https://pangolin.net/fcl"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://pangolin.net/fcl.html
https://pangolin.net/fcl
</a>
</div>
</FormLabel>
@@ -987,7 +987,7 @@ export default function GenerateLicenseKeyForm({
"signUpTerms.IAgreeToThe"
)}{" "}
<a
href="https://pangolin.net/terms-of-service.html"
href="https://pangolin.net/tos"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -1000,7 +1000,7 @@ export default function GenerateLicenseKeyForm({
"signUpTerms.and"
)}{" "}
<a
href="https://pangolin.net/privacy-policy.html"
href="https://pangolin.net/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -1044,12 +1044,12 @@ export default function GenerateLicenseKeyForm({
license
details:{" "}
<a
href="https://pangolin.net/fcl.html"
href="https://pangolin.net/fcl"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://pangolin.net/fcl.html
https://pangolin.net/fcl
</a>
</div>
</FormLabel>
+2 -2
View File
@@ -406,7 +406,7 @@ export default function HealthChecksTable({
}
return (
<Link
href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}
href={`/${orgId}/settings/resources/public/${r.resourceNiceId}`}
>
<Button variant="outline" size="sm">
{r.resourceName}
@@ -627,7 +627,7 @@ export default function HealthChecksTable({
</DropdownMenu>
{r.resourceId && r.resourceName && r.resourceNiceId ? (
<Link
href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}
href={`/${orgId}/settings/resources/public/${r.resourceNiceId}`}
>
<Button variant="outline" disabled={!isPaid}>
{t("edit")}
+7 -9
View File
@@ -24,17 +24,12 @@ import {
ArrowUp10Icon,
ChevronsUpDownIcon,
CircleSlash,
MoreHorizontal,
MoreHorizontal
} from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
startTransition,
useMemo,
useState,
useTransition
} from "react";
import { startTransition, useMemo, useState, useTransition } from "react";
import { useDebouncedCallback } from "use-debounce";
import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton";
@@ -110,7 +105,7 @@ export default function MachineClientsTable({
subnet: false,
userId: false,
niceId: false,
labels: false
labels: true
};
const refreshData = () => {
@@ -614,7 +609,10 @@ function MachineClientLabelCell({
}: MachineClientLabelCellProps) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const [localLabels, setLocalLabels] = useLocalLabels(client.labels, client.id);
const [localLabels, setLocalLabels] = useLocalLabels(
client.labels,
client.id
);
function toggleClientLabel(
label: SelectedLabel,
+9 -9
View File
@@ -389,7 +389,7 @@ export default function NewPricingLicenseForm({
{part}
{index === 0 && (
<a
href="https://pangolin.net/fcl.html"
href="https://pangolin.net/fcl"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -548,7 +548,7 @@ export default function NewPricingLicenseForm({
"signUpTerms.IAgreeToThe"
)}{" "}
<a
href="https://pangolin.net/terms-of-service.html"
href="https://pangolin.net/tos"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -561,7 +561,7 @@ export default function NewPricingLicenseForm({
"signUpTerms.and"
)}{" "}
<a
href="https://pangolin.net/privacy-policy.html"
href="https://pangolin.net/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -601,12 +601,12 @@ export default function NewPricingLicenseForm({
"generateLicenseKeyForm.form.complianceConfirmation"
)}{" "}
<a
href="https://pangolin.net/fcl.html"
href="https://pangolin.net/fcl"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://pangolin.net/fcl.html
https://pangolin.net/fcl
</a>
</div>
</FormLabel>
@@ -799,7 +799,7 @@ export default function NewPricingLicenseForm({
"signUpTerms.IAgreeToThe"
)}{" "}
<a
href="https://pangolin.net/terms-of-service.html"
href="https://pangolin.net/tos"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -812,7 +812,7 @@ export default function NewPricingLicenseForm({
"signUpTerms.and"
)}{" "}
<a
href="https://pangolin.net/privacy-policy.html"
href="https://pangolin.net/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -852,12 +852,12 @@ export default function NewPricingLicenseForm({
"generateLicenseKeyForm.form.complianceConfirmation"
)}{" "}
<a
href="https://pangolin.net/fcl.html"
href="https://pangolin.net/fcl"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://pangolin.net/fcl.html
https://pangolin.net/fcl
</a>
</div>
</FormLabel>
+62 -70
View File
@@ -1128,9 +1128,7 @@ export function PrivateResourceForm({
/>
</div>
)}
{(mode === "host" ||
(mode === "ssh" &&
sshServerMode !== "native")) && (
{(mode === "host" || mode === "ssh") && (
<div className="min-w-0">
<FormField
control={form.control}
@@ -1812,74 +1810,68 @@ export function PrivateResourceForm({
/>
</div>
{/* Auth Method (standard only) */}
{!isNative && (
<div className="space-y-3">
<p className="text-sm font-semibold">
{t("sshAuthenticationMethod")}
</p>
<FormField
control={form.control}
name="pamMode"
render={({ field }) => (
<FormItem>
<FormControl>
<StrategySelect<
"passthrough" | "push"
>
value={
field.value ??
"passthrough"
}
options={[
{
id: "passthrough",
title: t(
"sshAuthMethodManual"
),
description: t(
"sshAuthMethodManualDescription"
),
disabled:
sshSectionDisabled
},
{
id: "push",
title: t(
"sshAuthMethodAutomated"
),
description: t(
"sshAuthMethodAutomatedDescription"
),
disabled:
sshSectionDisabled
}
]}
onChange={(v) => {
if (
<div className="space-y-3">
<p className="text-sm font-semibold">
{t("sshAuthenticationMethod")}
</p>
<FormField
control={form.control}
name="pamMode"
render={({ field }) => (
<FormItem>
<FormControl>
<StrategySelect<
"passthrough" | "push"
>
value={
field.value ??
"passthrough"
}
options={[
{
id: "passthrough",
title: t(
"sshAuthMethodManual"
),
description: t(
"sshAuthMethodManualDescription"
),
disabled:
sshSectionDisabled
)
return;
field.onChange(v);
if (
v ===
"passthrough"
) {
form.setValue(
"authDaemonPort",
null
);
}
}}
cols={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
},
{
id: "push",
title: t(
"sshAuthMethodAutomated"
),
description: t(
"sshAuthMethodAutomatedDescription"
),
disabled:
sshSectionDisabled
}
]}
onChange={(v) => {
if (sshSectionDisabled)
return;
field.onChange(v);
if (
v === "passthrough"
) {
form.setValue(
"authDaemonPort",
null
);
}
}}
cols={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Daemon Location (standard + push) */}
{showDaemonLocation && (
@@ -129,7 +129,7 @@ type ClientResourcesTableProps = {
initialFilterSite?: Selectedsite | null;
};
export default function ClientResourcesTable({
export default function PrivateResourcesTable({
internalResources,
orgId,
pagination,
@@ -400,13 +400,16 @@ export default function ClientResourcesTable({
cell: ({ row }) => {
const resourceRow = row.original;
const display = formatDestinationDisplay(resourceRow);
return (
<CopyToClipboard
text={display}
isLink={false}
displayText={display}
/>
);
if (resourceRow.destination) {
return (
<CopyToClipboard
text={display}
isLink={false}
displayText={display}
/>
);
}
return <span>-</span>;
}
},
{
@@ -656,7 +659,7 @@ export default function ClientResourcesTable({
columnVisibility={{
niceId: false,
aliasAddress: false,
labels: false
labels: true
}}
stickyLeftColumn="name"
stickyRightColumn="actions"
+6 -4
View File
@@ -609,7 +609,7 @@ export default function ProxyResourcesTable({
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${resourceRow.orgId}/settings/resources/proxy/${resourceRow.nice}`}
href={`/${resourceRow.orgId}/settings/resources/public/${resourceRow.nice}`}
>
<DropdownMenuItem>
{t("viewSettings")}
@@ -628,7 +628,7 @@ export default function ProxyResourcesTable({
</DropdownMenuContent>
</DropdownMenu>
<Link
href={`/${resourceRow.orgId}/settings/resources/proxy/${resourceRow.nice}`}
href={`/${resourceRow.orgId}/settings/resources/public/${resourceRow.nice}`}
>
<Button variant={"outline"}>
{t("edit")}
@@ -744,7 +744,9 @@ export default function ProxyResourcesTable({
onPaginationChange={handlePaginationChange}
onAdd={() =>
startNavigation(() =>
router.push(`/${orgId}/settings/resources/proxy/create`)
router.push(
`/${orgId}/settings/resources/public/create`
)
)
}
addButtonText={t("resourceAdd")}
@@ -755,7 +757,7 @@ export default function ProxyResourcesTable({
columnVisibility={{
niceId: false,
protocol: false,
labels: false
labels: true
}}
stickyLeftColumn="name"
stickyRightColumn="actions"
+2 -4
View File
@@ -145,7 +145,7 @@ export default function ShareLinksTable({
const r = row.original;
return (
<Link
href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}
href={`/${orgId}/settings/resources/public/${r.resourceNiceId}`}
>
<Button variant="outline" size="sm">
{r.resourceName}
@@ -328,9 +328,7 @@ export default function ShareLinksTable({
onConfirm={async () =>
deleteSharelink(selectedLink.accessTokenId)
}
string={
selectedLink.title || selectedLink.resourceName
}
string={selectedLink.title || selectedLink.resourceName}
title={t("shareDelete")}
/>
)}
+2 -2
View File
@@ -554,7 +554,7 @@ export default function SignupForm({
"signUpTerms.IAgreeToThe"
)}{" "}
<a
href="https://pangolin.net/terms-of-service.html"
href="https://pangolin.net/tos"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
@@ -567,7 +567,7 @@ export default function SignupForm({
"signUpTerms.and"
)}{" "}
<a
href="https://pangolin.net/privacy-policy.html"
href="https://pangolin.net/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
+4 -4
View File
@@ -421,15 +421,15 @@ export default function SiteResourcesOverview({
publicList.length === 0 &&
privateList.length === 0;
const publicViewAllHref = `/${orgId}/settings/resources/proxy?siteId=${siteId}`;
const privateViewAllHref = `/${orgId}/settings/resources/client?siteId=${siteId}`;
const publicViewAllHref = `/${orgId}/settings/resources/public?siteId=${siteId}`;
const privateViewAllHref = `/${orgId}/settings/resources/private?siteId=${siteId}`;
const publicRows = publicList.map((r) => ({
key: r.resourceId,
meta: <PublicResourceMeta resource={r} />,
name: r.name,
access: <PublicAccessMethod resource={r} />,
editHref: `/${orgId}/settings/resources/proxy/${r.niceId}`
editHref: `/${orgId}/settings/resources/public/${r.niceId}`
}));
const privateRows = privateList.map((row) => {
@@ -442,7 +442,7 @@ export default function SiteResourcesOverview({
meta: <PrivateResourceMeta row={row} />,
name: row.name,
access: <PrivateAccessMethod row={row} />,
editHref: `/${orgId}/settings/resources/client?${qs.toString()}`
editHref: `/${orgId}/settings/resources/private?${qs.toString()}`
};
});
+4 -4
View File
@@ -36,7 +36,7 @@ import {
ArrowUpRight,
ChevronDown,
ChevronsUpDownIcon,
MoreHorizontal,
MoreHorizontal
} from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
@@ -506,7 +506,7 @@ export default function SitesTable({
</Link>
<Link
className="block w-full"
href={`/${siteRow.orgId}/settings/resources/proxy?siteId=${siteRow.id}`}
href={`/${siteRow.orgId}/settings/resources/public?siteId=${siteRow.id}`}
>
<DropdownMenuItem>
{t("sitesTableViewPublicResources")}
@@ -514,7 +514,7 @@ export default function SitesTable({
</Link>
<Link
className="block w-full"
href={`/${siteRow.orgId}/settings/resources/client?siteId=${siteRow.id}`}
href={`/${siteRow.orgId}/settings/resources/private?siteId=${siteRow.id}`}
>
<DropdownMenuItem>
{t(
@@ -670,7 +670,7 @@ export default function SitesTable({
nice: false,
exitNode: false,
address: false,
labels: false
labels: true
}}
enableColumnVisibility
stickyLeftColumn="name"