Compare commits

...

8 Commits

Author SHA1 Message Date
Owen eca87b66f0 Use the create api 2026-05-26 17:11:45 -07:00
Owen cc8c89eeae Cleaning up some react 2026-05-26 16:53:22 -07:00
Owen 6ea4aa1920 Suppoter key 2026-05-26 16:35:28 -07:00
Owen f12451b8f9 Consolidate target components 2026-05-26 16:33:54 -07:00
Owen 0d4bb65a92 Adjusting the create ui 2026-05-26 16:10:06 -07:00
Owen d47ad9ac40 Fix height problem 2026-05-26 16:08:52 -07:00
Owen f81ae24ba7 Clean up the ui a bit 2026-05-26 15:05:30 -07:00
Owen df1e28aabd Pass one on the new create screen 2026-05-26 14:29:13 -07:00
11 changed files with 2001 additions and 2339 deletions
+10 -5
View File
@@ -220,8 +220,9 @@
"resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. Requires sites to connect to a remote node.",
"resourceCreate": "Create Resource",
"resourceCreateDescription": "Follow the steps below to create a new resource",
"resourceCreateGeneralDescription": "Configure the basic resource settings including the name and the type",
"resourceSeeAll": "See All Resources",
"resourceInfo": "Resource Information",
"resourceCreateGeneral": "General",
"resourceNameDescription": "This is the display name for the resource.",
"siteSelect": "Select site",
"siteSearch": "Search site",
@@ -231,12 +232,15 @@
"noCountryFound": "No country found.",
"siteSelectionDescription": "This site will provide connectivity to the target.",
"resourceType": "Resource Type",
"resourceTypeDescription": "Determine how to access the resource",
"resourceTypeDescription": "This controls the resource protocol and how it will be rendered in the browser. This cant be changed later.",
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
"resourceHTTPSSettings": "HTTPS Settings",
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
"resourcePortDescription": "The external port on the Pangolin instance or node where the resource will be accessible.",
"domainType": "Domain Type",
"subdomain": "Subdomain",
"baseDomain": "Base Domain",
"configure": "Configure",
"subdomnainDescription": "The subdomain where the resource will be accessible.",
"resourceRawSettings": "TCP/UDP Settings",
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP",
@@ -1982,9 +1986,9 @@
"sshServerMode": "Mode",
"sshServerModeStandard": "Standard SSH Server",
"sshServerModePangolin": "Pangolin SSH",
"sshServerModeStandardDescription": "Uses a Pangolin auth daemon to manage SSH authentication on the site or remote host.",
"sshServerModeStandardDescription": "Routes commands over network to an SSH server such as OpenSSH.",
"sshServerModeNative": "Native SSH Server",
"sshServerModeNativeDescription": "SSH authentication is handled natively by an existing SSH server without a separate auth daemon.",
"sshServerModeNativeDescription": "Executes commands directly on the host via the Site Connector. No network config required.",
"sshAuthenticationMethod": "Authentication Method",
"sshAuthMethodManual": "Manual Authentication",
"sshAuthMethodManualDescription": "Requires existing host credentials. Bypasses automatic provisioning.",
@@ -3354,5 +3358,6 @@
"memberPortalResourceDisabled": "Resource Disabled",
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
"memberPortalPrevious": "Previous",
"memberPortalNext": "Next"
"memberPortalNext": "Next",
"httpSettings": "HTTP Settings"
}
+23 -3
View File
@@ -23,7 +23,10 @@ import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { getUniqueResourceName } from "@server/db/names";
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
import {
validateAndConstructDomain,
checkWildcardDomainConflict
} from "@server/lib/domainUtils";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
@@ -40,7 +43,12 @@ const createHttpResourceSchema = z
protocol: z.enum(["tcp", "udp"]),
domainId: z.string(),
stickySession: z.boolean().optional(),
postAuthPath: z.string().nullable().optional()
postAuthPath: z.string().nullable().optional(),
browserAccessType: z.enum(["http", "ssh", "rdp", "vnc"]).optional(),
// SSH Settings
pamMode: z.enum(["passthrough", "push"]).optional(),
authDaemonPort: z.int().positive().optional(),
authDaemonMode: z.enum(["site", "remote", "native"]).optional()
})
.refine(
(data) => {
@@ -198,7 +206,15 @@ async function createHttpResource(
);
}
const { name, domainId, postAuthPath } = parsedBody.data;
const {
name,
domainId,
postAuthPath,
browserAccessType,
authDaemonPort,
authDaemonMode,
pamMode
} = parsedBody.data;
const subdomain = parsedBody.data.subdomain;
const stickySession = parsedBody.data.stickySession;
@@ -323,6 +339,10 @@ async function createHttpResource(
name,
subdomain: finalSubdomain,
http: true,
browserAccessType: browserAccessType,
pamMode: pamMode,
authDaemonMode: authDaemonMode,
authDaemonPort: authDaemonPort,
protocol: "tcp",
ssl: true,
stickySession: stickySession,
@@ -68,6 +68,7 @@ const createSiteResourceSchema = z
disableIcmp: z.boolean().optional(),
authDaemonPort: z.int().positive().optional(),
authDaemonMode: z.enum(["site", "remote"]).optional(),
pamMode: z.enum(["passthrough", "push"]).optional(),
domainId: z.string().optional(), // only used for http mode, we need this to verify the alias is unique within the org
subdomain: z.string().optional() // only used for http mode, we need this to verify the alias is unique within the org
})
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -6,9 +6,7 @@ import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
@@ -36,7 +34,6 @@ import {
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
@@ -55,18 +52,11 @@ import {
SettingsSectionTitle,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionFooter,
SettingsSectionForm
SettingsSectionFooter
} from "@app/components/Settings";
import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules";
import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { ArrowUpDown, Check, InfoIcon, X, ChevronsUpDown } from "lucide-react";
import {
InfoSection,
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import { InfoPopup } from "@app/components/ui/info-popup";
import {
isValidCIDR,
@@ -78,7 +68,11 @@ import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { COUNTRIES } from "@server/db/countries";
import { MAJOR_ASNS } from "@server/db/asns";
import { REGIONS, getRegionNameById, isValidRegionId } from "@server/db/regions";
import {
REGIONS,
getRegionNameById,
isValidRegionId
} from "@server/db/regions";
import {
Command,
CommandEmpty,
@@ -109,25 +103,23 @@ type LocalRule = ArrayElement<ListResourceRulesResponse["rules"]> & {
export default function ResourceRules(props: {
params: Promise<{ resourceId: number }>;
}) {
const params = use(props.params);
const { resource, updateResource } = useResourceContext();
const api = createApiClient(useEnvContext());
const [rules, setRules] = useState<LocalRule[]>([]);
const [rulesToRemove, setRulesToRemove] = useState<number[]>([]);
const [loading, setLoading] = useState(false);
const [pageLoading, setPageLoading] = useState(true);
const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules ?? false);
const [rulesEnabled, setRulesEnabled] = useState(
resource.applyRules ?? false
);
useEffect(() => {
setRulesEnabled(resource.applyRules);
}, [resource.applyRules]);
const [openCountrySelect, setOpenCountrySelect] = useState(false);
const [countrySelectValue, setCountrySelectValue] = useState("");
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
useState(false);
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] =
useState(false);
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false);
const [openAddRuleRegionSelect, setOpenAddRuleRegionSelect] =
useState(false);
const router = useRouter();
@@ -157,7 +149,10 @@ export default function ResourceRules(props: {
resolver: zodResolver(addRuleSchema),
defaultValues: {
action: "ACCEPT",
match: "PATH",
match:
resource.http && resource.browserAccessType == "http"
? "PATH"
: "IP",
value: ""
}
});
@@ -270,16 +265,12 @@ export default function ResourceRules(props: {
setLoading(false);
return;
}
if (
data.match === "REGION" &&
!isValidRegionId(data.value)
) {
if (data.match === "REGION" && !isValidRegionId(data.value)) {
toast({
variant: "destructive",
title: t("rulesErrorInvalidRegion"),
description:
t("rulesErrorInvalidRegionDescription") ||
"Invalid region."
t("rulesErrorInvalidRegionDescription") || "Invalid region."
});
setLoading(false);
return;
@@ -564,12 +555,24 @@ export default function ResourceRules(props: {
<Select
defaultValue={row.original.match}
onValueChange={(
value: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN" | "REGION"
value:
| "CIDR"
| "IP"
| "PATH"
| "COUNTRY"
| "ASN"
| "REGION"
) =>
updateRule(row.original.ruleId, {
match: value,
value:
value === "COUNTRY" ? "US" : value === "ASN" ? "AS15169" : value === "REGION" ? "021" : row.original.value
value === "COUNTRY"
? "US"
: value === "ASN"
? "AS15169"
: value === "REGION"
? "021"
: row.original.value
})
}
>
@@ -577,7 +580,12 @@ export default function ResourceRules(props: {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="PATH">{RuleMatch.PATH}</SelectItem>
{resource.http &&
resource.browserAccessType == "http" && (
<SelectItem value="PATH">
{RuleMatch.PATH}
</SelectItem>
)}
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
{isMaxmindAvailable && (
@@ -669,14 +677,14 @@ export default function ResourceRules(props: {
>
{row.original.value
? (() => {
const found = MAJOR_ASNS.find(
const found = MAJOR_ASNS.find(
(asn) =>
asn.code ===
row.original.value
);
return found
? `${found.name} (${row.original.value})`
: `Custom (${row.original.value})`;
);
return found
? `${found.name} (${row.original.value})`
: `Custom (${row.original.value})`;
})()
: "Select ASN"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
@@ -755,7 +763,9 @@ export default function ResourceRules(props: {
className="min-w-[200px] justify-between"
>
{(() => {
const regionName = getRegionNameById(row.original.value);
const regionName = getRegionNameById(
row.original.value
);
if (!regionName) {
return t("selectRegion");
}
@@ -774,7 +784,10 @@ export default function ResourceRules(props: {
{t("noRegionFound")}
</CommandEmpty>
{REGIONS.map((continent) => (
<CommandGroup key={continent.id} heading={t(continent.name)}>
<CommandGroup
key={continent.id}
heading={t(continent.name)}
>
<CommandItem
value={continent.id}
keywords={[
@@ -790,38 +803,48 @@ export default function ResourceRules(props: {
>
<Check
className={`mr-2 h-4 w-4 ${
row.original.value === continent.id
row.original.value ===
continent.id
? "opacity-100"
: "opacity-0"
}`}
/>
{t(continent.name)} ({continent.id})
{t(continent.name)} (
{continent.id})
</CommandItem>
{continent.includes.map((subregion) => (
<CommandItem
key={subregion.id}
value={subregion.id}
keywords={[
t(subregion.name),
subregion.id
]}
onSelect={() => {
updateRule(
row.original.ruleId,
{ value: subregion.id }
);
}}
>
<Check
className={`mr-2 h-4 w-4 ${
row.original.value === subregion.id
? "opacity-100"
: "opacity-0"
}`}
/>
{t(subregion.name)} ({subregion.id})
</CommandItem>
))}
{continent.includes.map(
(subregion) => (
<CommandItem
key={subregion.id}
value={subregion.id}
keywords={[
t(subregion.name),
subregion.id
]}
onSelect={() => {
updateRule(
row.original
.ruleId,
{
value: subregion.id
}
);
}}
>
<Check
className={`mr-2 h-4 w-4 ${
row.original
.value ===
subregion.id
? "opacity-100"
: "opacity-0"
}`}
/>
{t(subregion.name)} (
{subregion.id})
</CommandItem>
)
)}
</CommandGroup>
))}
</CommandList>
@@ -1018,13 +1041,15 @@ export default function ResourceRules(props: {
<SelectValue />
</SelectTrigger>
<SelectContent>
{resource.http && (
<SelectItem value="PATH">
{
RuleMatch.PATH
}
</SelectItem>
)}
{resource.http &&
resource.browserAccessType ==
"http" && (
<SelectItem value="PATH">
{
RuleMatch.PATH
}
</SelectItem>
)}
<SelectItem value="IP">
{RuleMatch.IP}
</SelectItem>
@@ -1311,8 +1336,8 @@ export default function ResourceRules(props: {
</PopoverContent>
</Popover>
) : addRuleForm.watch(
"match"
) === "REGION" ? (
"match"
) === "REGION" ? (
<Popover
open={
openAddRuleRegionSelect
@@ -1334,8 +1359,16 @@ export default function ResourceRules(props: {
>
{field.value
? (() => {
const regionName = getRegionNameById(field.value);
const translatedName = regionName ? t(regionName) : field.value;
const regionName =
getRegionNameById(
field.value
);
const translatedName =
regionName
? t(
regionName
)
: field.value;
return `${translatedName} (${field.value})`;
})()
: t(
@@ -1357,43 +1390,31 @@ export default function ResourceRules(props: {
"noRegionFound"
)}
</CommandEmpty>
{REGIONS.map((continent) => (
<CommandGroup key={continent.id} heading={t(continent.name)}>
<CommandItem
value={continent.id}
keywords={[
t(continent.name),
{REGIONS.map(
(
continent
) => (
<CommandGroup
key={
continent.id
]}
onSelect={() => {
field.onChange(
continent.id
);
setOpenAddRuleRegionSelect(
false
);
}}
}
heading={t(
continent.name
)}
>
<Check
className={`mr-2 h-4 w-4 ${
field.value === continent.id
? "opacity-100"
: "opacity-0"
}`}
/>
{t(continent.name)} ({continent.id})
</CommandItem>
{continent.includes.map((subregion) => (
<CommandItem
key={subregion.id}
value={subregion.id}
value={
continent.id
}
keywords={[
t(subregion.name),
subregion.id
t(
continent.name
),
continent.id
]}
onSelect={() => {
field.onChange(
subregion.id
continent.id
);
setOpenAddRuleRegionSelect(
false
@@ -1402,16 +1423,71 @@ export default function ResourceRules(props: {
>
<Check
className={`mr-2 h-4 w-4 ${
field.value === subregion.id
field.value ===
continent.id
? "opacity-100"
: "opacity-0"
}`}
/>
{t(subregion.name)} ({subregion.id})
{t(
continent.name
)}{" "}
(
{
continent.id
}
)
</CommandItem>
))}
</CommandGroup>
))}
{continent.includes.map(
(
subregion
) => (
<CommandItem
key={
subregion.id
}
value={
subregion.id
}
keywords={[
t(
subregion.name
),
subregion.id
]}
onSelect={() => {
field.onChange(
subregion.id
);
setOpenAddRuleRegionSelect(
false
);
}}
>
<Check
className={`mr-2 h-4 w-4 ${
field.value ===
subregion.id
? "opacity-100"
: "opacity-0"
}`}
/>
{t(
subregion.name
)}{" "}
(
{
subregion.id
}
)
</CommandItem>
)
)}
</CommandGroup>
)
)}
</CommandList>
</Command>
</PopoverContent>
File diff suppressed because it is too large Load Diff
-6
View File
@@ -3,7 +3,6 @@
import type { SidebarNavSection } from "@app/app/navigation";
import { OrgSelector } from "@app/components/OrgSelector";
import { SidebarNav } from "@app/components/SidebarNav";
import SupporterStatus from "@app/components/SupporterStatus";
import {
Tooltip,
TooltipContent,
@@ -258,11 +257,6 @@ export function LayoutSidebar({
/>
</div>
)}
{build === "oss" && (
<div className="px-4">
<SupporterStatus isCollapsed={isSidebarCollapsed} />
</div>
)}
{build === "saas" && (
<div className="px-4">
<SidebarSupportButton
-2
View File
@@ -19,7 +19,6 @@ import { Laptop, LogOut, Moon, Sun, Smartphone, Trash2 } from "lucide-react";
import { useTheme } from "next-themes";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { build } from "@server/build";
import { useState } from "react";
import { useUserContext } from "@app/hooks/useUserContext";
import Disable2FaForm from "./Disable2FaForm";
@@ -27,7 +26,6 @@ import SecurityKeyForm from "./SecurityKeyForm";
import Enable2FaDialog from "./Enable2FaDialog";
import ChangePasswordDialog from "./ChangePasswordDialog";
import ViewDevicesDialog from "./ViewDevicesDialog";
import SupporterStatus from "./SupporterStatus";
import { UserType } from "@server/types/UserTypes";
import LocaleSwitcher from "@app/components/LocaleSwitcher";
import { useTranslations } from "next-intl";
+7 -9
View File
@@ -1,14 +1,10 @@
"use client";
import Image from "next/image";
import { Separator } from "@app/components/ui/separator";
// THIS IS DEPRECATED AND IS NO LONGER SHOWED TO THE USER WITH THE DISCONTINUATION
// OF THE SUPPORTER PROGRAM. IT MAY BE REMOVED IN A FUTURE UPDATE.
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
import { useState, useTransition } from "react";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import {
Tooltip,
TooltipContent,
@@ -44,7 +40,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { AxiosResponse } from "axios";
import { ValidateSupporterKeyResponse } from "@server/routers/supporterKey";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
Card,
CardContent,
@@ -228,7 +223,10 @@ export default function SupporterStatus({
<div className="my-4 p-4 border border-blue-500/50 bg-blue-500/10 rounded-lg">
<p className="text-sm">
<strong>Business & Enterprise Users:</strong> For larger organizations or teams requiring advanced features, consider our self-serve enterprise license and Enterprise Edition.{" "}
<strong>Business & Enterprise Users:</strong>{" "}
For larger organizations or teams requiring
advanced features, consider our self-serve
enterprise license and Enterprise Edition.{" "}
<Link
href="https://pangolin.net/pricing#Self-Hosted"
target="_blank"
@@ -41,23 +41,23 @@ export function MultiSelectTagInput<T extends TagValue>({
variant: "outline"
}),
"justify-between w-full inline-flex",
"text-muted-foreground pl-1.5 cursor-text h-auto py-1",
"text-muted-foreground pl-1.5 cursor-text h-9 py-0",
"hover:bg-transparent hover:text-muted-foreground",
props.disabled && "pointer-events-none opacity-50"
)}
>
<span
className={cn(
"inline-flex items-center gap-1",
"overflow-x-auto flex-wrap h-auto"
"inline-flex items-center gap-1 min-w-0 flex-1",
"overflow-x-auto flex-nowrap h-full"
)}
>
{props.value.map((option) => (
<span
key={option.id}
className={cn(
"bg-muted-foreground/10 font-normal text-foreground rounded-sm",
"py-1 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5"
"bg-muted-foreground/10 font-normal text-foreground rounded-sm flex-none",
"py-0.5 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5"
)}
onClick={(e) => e.stopPropagation()}
>