Merge branch 'dev' of github.com:fosrl/pangolin into dev

This commit is contained in:
Owen
2025-10-27 17:55:10 -07:00
5 changed files with 194 additions and 306 deletions

View File

@@ -1,3 +1,4 @@
import logger from "@server/logger";
import axios from "axios"; import axios from "axios";
let serverIp: string | null = null; let serverIp: string | null = null;
@@ -13,8 +14,8 @@ export async function fetchServerIp() {
try { try {
const response = await axios.get(url, { timeout: 5000 }); const response = await axios.get(url, { timeout: 5000 });
serverIp = response.data.trim(); serverIp = response.data.trim();
console.log("Detected public IP:", serverIp); logger.debug("Detected public IP: " + serverIp);
return; return;
} catch (err: any) { } catch (err: any) {
console.warn(`Failed to fetch server IP from ${url}: ${err.message || err.code}`); console.warn(`Failed to fetch server IP from ${url}: ${err.message || err.code}`);
} }

View File

@@ -72,12 +72,11 @@ interface TunnelTypeOption {
} }
type Commands = { type Commands = {
mac: Record<string, string[]>; unix: Record<string, string[]>;
linux: Record<string, string[]>;
windows: Record<string, string[]>; windows: Record<string, string[]>;
}; };
const platforms = ["linux", "mac", "windows"] as const; const platforms = ["unix", "windows"] as const;
type Platform = (typeof platforms)[number]; type Platform = (typeof platforms)[number];
@@ -128,8 +127,8 @@ export default function Page() {
number | null number | null
>(null); >(null);
const [platform, setPlatform] = useState<Platform>("linux"); const [platform, setPlatform] = useState<Platform>("unix");
const [architecture, setArchitecture] = useState("amd64"); const [architecture, setArchitecture] = useState("All");
const [commands, setCommands] = useState<Commands | null>(null); const [commands, setCommands] = useState<Commands | null>(null);
const [olmId, setOlmId] = useState(""); const [olmId, setOlmId] = useState("");
@@ -148,43 +147,15 @@ export default function Page() {
version: string version: string
) => { ) => {
const commands = { const commands = {
mac: { unix: {
"Apple Silicon (arm64)": [ All: [
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
],
"Intel x64 (amd64)": [
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
]
},
linux: {
amd64: [
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
],
arm64: [
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
],
arm32: [
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
],
arm32v6: [
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
],
riscv64: [
`curl -fsSL https://pangolin.net/get-olm.sh | bash`, `curl -fsSL https://pangolin.net/get-olm.sh | bash`,
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
] ]
}, },
windows: { windows: {
x64: [ x64: [
`# Download and run the installer`,
`curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`, `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`,
`# Then run olm with your credentials`,
`olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}` `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
] ]
} }
@@ -194,10 +165,8 @@ export default function Page() {
const getArchitectures = () => { const getArchitectures = () => {
switch (platform) { switch (platform) {
case "linux": case "unix":
return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; return ["All"];
case "mac":
return ["Apple Silicon (arm64)", "Intel x64 (amd64)"];
case "windows": case "windows":
return ["x64"]; return ["x64"];
default: default:
@@ -209,12 +178,12 @@ export default function Page() {
switch (platformName) { switch (platformName) {
case "windows": case "windows":
return "Windows"; return "Windows";
case "mac": case "unix":
return "macOS"; return "Unix & macOS";
case "docker": case "docker":
return "Docker"; return "Docker";
default: default:
return "Linux"; return "Unix & macOS";
} }
}; };
@@ -249,8 +218,8 @@ export default function Page() {
switch (platformName) { switch (platformName) {
case "windows": case "windows":
return <FaWindows className="h-4 w-4 mr-2" />; return <FaWindows className="h-4 w-4 mr-2" />;
case "mac": case "unix":
return <FaApple className="h-4 w-4 mr-2" />; return <Terminal className="h-4 w-4 mr-2" />;
case "docker": case "docker":
return <FaDocker className="h-4 w-4 mr-2" />; return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes": case "kubernetes":

View File

@@ -51,16 +51,8 @@ import { useTranslations } from "next-intl";
import { build } from "@server/build"; import { build } from "@server/build";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
import { ChevronDown } from "lucide-react";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { Alert, AlertDescription } from "@app/components/ui/alert";
// Session length options in hours // Session length options in hours
const SESSION_LENGTH_OPTIONS = [ const SESSION_LENGTH_OPTIONS = [
@@ -284,11 +276,6 @@ export default function GeneralPage() {
} }
} }
const getLabelForValue = (value: number) => {
const option = LOG_RETENTION_OPTIONS.find((opt) => opt.value === value);
return option ? t(option.label) : `${value} days`;
};
return ( return (
<SettingsContainer> <SettingsContainer>
<ConfirmDeleteDialog <ConfirmDeleteDialog
@@ -402,21 +389,22 @@ export default function GeneralPage() {
{t("logRetentionRequestLabel")} {t("logRetentionRequestLabel")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<DropdownMenu> <Select
<DropdownMenuTrigger value={field.value.toString()}
asChild onValueChange={(value) =>
> field.onChange(
<Button parseInt(value, 10)
variant="outline" )
className="w-full justify-between" }
> >
{getLabelForValue( <SelectTrigger>
field.value <SelectValue
placeholder={t(
"selectLogRetention"
)} )}
<ChevronDown className="h-4 w-4" /> />
</Button> </SelectTrigger>
</DropdownMenuTrigger> <SelectContent>
<DropdownMenuContent>
{LOG_RETENTION_OPTIONS.filter( {LOG_RETENTION_OPTIONS.filter(
(option) => { (option) => {
if ( if (
@@ -431,29 +419,16 @@ export default function GeneralPage() {
return true; return true;
} }
).map((option) => ( ).map((option) => (
<DropdownMenuItem <SelectItem
key={ key={option.value}
option.value value={option.value.toString()}
}
onClick={() =>
field.onChange(
option.value
)
}
> >
{t( {t(option.label)}
option.label </SelectItem>
)}
</DropdownMenuItem>
))} ))}
</DropdownMenuContent> </SelectContent>
</DropdownMenu> </Select>
</FormControl> </FormControl>
<FormDescription>
{t(
"logRetentionRequestDescription"
)}
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -461,163 +436,153 @@ export default function GeneralPage() {
{build != "oss" && ( {build != "oss" && (
<> <>
{build == "saas" && <SecurityFeaturesAlert />
!subscription?.subscribed ? (
<Alert
variant="info"
className="mb-6"
>
<AlertDescription>
{t(
"subscriptionRequiredToUse"
)}
</AlertDescription>
</Alert>
) : null}
{build == "enterprise" &&
!isUnlocked() ? (
<Alert
variant="info"
className="mb-6"
>
<AlertDescription>
{t("licenseRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
<FormField <FormField
control={form.control} control={form.control}
name="settingsLogRetentionDaysAccess" name="settingsLogRetentionDaysAccess"
render={({ field }) => ( render={({ field }) => {
<FormItem> const isDisabled =
<FormLabel> (build == "saas" &&
{t( !subscription
"logRetentionAccessLabel" ?.subscribed) ||
)} (build == "enterprise" &&
</FormLabel> !isUnlocked());
<FormControl>
<DropdownMenu> return (
<DropdownMenuTrigger <FormItem>
asChild <FormLabel>
> {t(
<Button "logRetentionAccessLabel"
variant="outline" )}
className="w-full justify-between" </FormLabel>
disabled={ <FormControl>
(build == <Select
"saas" && value={field.value.toString()}
!subscription?.subscribed) || onValueChange={(
(build == value
"enterprise" && ) => {
!isUnlocked()) if (
!isDisabled
) {
field.onChange(
parseInt(
value,
10
)
);
} }
> }}
{getLabelForValue( disabled={
field.value isDisabled
)} }
<ChevronDown className="h-4 w-4" /> >
</Button> <SelectTrigger>
</DropdownMenuTrigger> <SelectValue
<DropdownMenuContent className="w-full"> placeholder={t(
{LOG_RETENTION_OPTIONS.map( "selectLogRetention"
( )}
option />
) => ( </SelectTrigger>
<DropdownMenuItem <SelectContent>
key={ {LOG_RETENTION_OPTIONS.map(
option.value (
} option
onClick={() => ) => (
field.onChange( <SelectItem
key={
option.value option.value
) }
} value={
> option.value.toString()
{t( }
option.label >
)} {t(
</DropdownMenuItem> option.label
) )}
)} </SelectItem>
</DropdownMenuContent> )
</DropdownMenu> )}
</FormControl> </SelectContent>
<FormDescription> </Select>
{t( </FormControl>
"logRetentionAccessDescription" <FormMessage />
)} </FormItem>
</FormDescription> );
<FormMessage /> }}
</FormItem>
)}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="settingsLogRetentionDaysAction" name="settingsLogRetentionDaysAction"
render={({ field }) => ( render={({ field }) => {
<FormItem> const isDisabled =
<FormLabel> (build == "saas" &&
{t( !subscription
"logRetentionActionLabel" ?.subscribed) ||
)} (build == "enterprise" &&
</FormLabel> !isUnlocked());
<FormControl>
<DropdownMenu> return (
<DropdownMenuTrigger <FormItem>
asChild <FormLabel>
> {t(
<Button "logRetentionActionLabel"
variant="outline" )}
className="w-full justify-between" </FormLabel>
disabled={ <FormControl>
(build == <Select
"saas" && value={field.value.toString()}
!subscription?.subscribed) || onValueChange={(
(build == value
"enterprise" && ) => {
!isUnlocked()) if (
!isDisabled
) {
field.onChange(
parseInt(
value,
10
)
);
} }
> }}
{getLabelForValue( disabled={
field.value isDisabled
)} }
<ChevronDown className="h-4 w-4" /> >
</Button> <SelectTrigger>
</DropdownMenuTrigger> <SelectValue
<DropdownMenuContent className="w-full"> placeholder={t(
{LOG_RETENTION_OPTIONS.map( "selectLogRetention"
( )}
option />
) => ( </SelectTrigger>
<DropdownMenuItem <SelectContent>
key={ {LOG_RETENTION_OPTIONS.map(
option.value (
} option
onClick={() => ) => (
field.onChange( <SelectItem
key={
option.value option.value
) }
} value={
> option.value.toString()
{t( }
option.label >
)} {t(
</DropdownMenuItem> option.label
) )}
)} </SelectItem>
</DropdownMenuContent> )
</DropdownMenu> )}
</FormControl> </SelectContent>
<FormDescription> </Select>
{t( </FormControl>
"logRetentionActionDescription" <FormMessage />
)} </FormItem>
</FormDescription> );
<FormMessage /> }}
</FormItem>
)}
/> />
</> </>
)} )}

View File

@@ -79,9 +79,7 @@ interface RemoteExitNodeOption {
} }
type Commands = { type Commands = {
mac: Record<string, string[]>; unix: Record<string, string[]>;
linux: Record<string, string[]>;
freebsd: Record<string, string[]>;
windows: Record<string, string[]>; windows: Record<string, string[]>;
docker: Record<string, string[]>; docker: Record<string, string[]>;
kubernetes: Record<string, string[]>; kubernetes: Record<string, string[]>;
@@ -90,13 +88,11 @@ type Commands = {
}; };
const platforms = [ const platforms = [
"linux", "unix",
"docker", "docker",
"kubernetes", "kubernetes",
"podman", "podman",
"mac",
"windows", "windows",
"freebsd",
"nixos" "nixos"
] as const; ] as const;
@@ -190,7 +186,7 @@ export default function Page() {
const [loadingPage, setLoadingPage] = useState(true); const [loadingPage, setLoadingPage] = useState(true);
const [platform, setPlatform] = useState<Platform>("linux"); const [platform, setPlatform] = useState<Platform>("unix");
const [architecture, setArchitecture] = useState("amd64"); const [architecture, setArchitecture] = useState("amd64");
const [commands, setCommands] = useState<Commands | null>(null); const [commands, setCommands] = useState<Commands | null>(null);
@@ -250,47 +246,11 @@ PersistentKeepalive = 5`;
: ""; : "";
const commands = { const commands = {
mac: { unix: {
All: [ All: [
`curl -fsSL https://pangolin.net/get-newt.sh | bash`, `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
`newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
] ]
// "Intel x64 (amd64)": [
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
// ]
},
linux: {
All: [
`curl -fsSL https://pangolin.net/get-newt.sh | bash`,
`newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
// arm64: [
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
// ],
// arm32: [
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
// ],
// arm32v6: [
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
// ],
// riscv64: [
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
// ]
},
freebsd: {
All: [
`curl -fsSL https://pangolin.net/get-newt.sh | bash`,
`newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
// arm64: [
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
// ]
}, },
windows: { windows: {
x64: [ x64: [
@@ -353,9 +313,6 @@ WantedBy=default.target`
All: [ All: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
] ]
// aarch64: [
// `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
// ]
} }
}; };
setCommands(commands); setCommands(commands);
@@ -363,11 +320,7 @@ WantedBy=default.target`
const getArchitectures = () => { const getArchitectures = () => {
switch (platform) { switch (platform) {
case "linux": case "unix":
// return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"];
return ["All"];
case "mac":
// return ["Apple Silicon (arm64)", "Intel x64 (amd64)"];
return ["All"]; return ["All"];
case "windows": case "windows":
return ["x64"]; return ["x64"];
@@ -377,11 +330,7 @@ WantedBy=default.target`
return ["Helm Chart"]; return ["Helm Chart"];
case "podman": case "podman":
return ["Podman Quadlet", "Podman Run"]; return ["Podman Quadlet", "Podman Run"];
case "freebsd":
// return ["amd64", "arm64"];
return ["All"];
case "nixos": case "nixos":
// return ["x86_64", "aarch64"];
return ["All"]; return ["All"];
default: default:
return ["x64"]; return ["x64"];
@@ -392,20 +341,18 @@ WantedBy=default.target`
switch (platformName) { switch (platformName) {
case "windows": case "windows":
return "Windows"; return "Windows";
case "mac": case "unix":
return "macOS"; return "Unix & macOS";
case "docker": case "docker":
return "Docker"; return "Docker";
case "kubernetes": case "kubernetes":
return "Kubernetes"; return "Kubernetes";
case "podman": case "podman":
return "Podman"; return "Podman";
case "freebsd":
return "FreeBSD";
case "nixos": case "nixos":
return "NixOS"; return "NixOS";
default: default:
return "Linux"; return "Unix / macOS";
} }
}; };
@@ -440,16 +387,14 @@ WantedBy=default.target`
switch (platformName) { switch (platformName) {
case "windows": case "windows":
return <FaWindows className="h-4 w-4 mr-2" />; return <FaWindows className="h-4 w-4 mr-2" />;
case "mac": case "unix":
return <FaApple className="h-4 w-4 mr-2" />; return <Terminal className="h-4 w-4 mr-2" />;
case "docker": case "docker":
return <FaDocker className="h-4 w-4 mr-2" />; return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes": case "kubernetes":
return <SiKubernetes className="h-4 w-4 mr-2" />; return <SiKubernetes className="h-4 w-4 mr-2" />;
case "podman": case "podman":
return <FaCubes className="h-4 w-4 mr-2" />; return <FaCubes className="h-4 w-4 mr-2" />;
case "freebsd":
return <FaFreebsd className="h-4 w-4 mr-2" />;
case "nixos": case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />; return <SiNixos className="h-4 w-4 mr-2" />;
default: default:

View File

@@ -18,6 +18,7 @@ import { NextIntlClientProvider } from "next-intl";
import { getLocale } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { Toaster } from "@app/components/ui/toaster"; import { Toaster } from "@app/components/ui/toaster";
import { build } from "@server/build"; import { build } from "@server/build";
import Script from "next/script";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
@@ -62,9 +63,9 @@ export default async function RootLayout({
if (build === "enterprise") { if (build === "enterprise") {
const licenseStatusRes = await cache( const licenseStatusRes = await cache(
async () => async () =>
await priv.get<AxiosResponse<GetLicenseStatusResponse>>( await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status" "/license/status"
) )
)(); )();
licenseStatus = licenseStatusRes.data.data; licenseStatus = licenseStatusRes.data.data;
} else if (build === "saas") { } else if (build === "saas") {
@@ -84,6 +85,13 @@ export default async function RootLayout({
return ( return (
<html suppressHydrationWarning lang={locale}> <html suppressHydrationWarning lang={locale}>
<body className={`${font.className} h-screen overflow-hidden`}> <body className={`${font.className} h-screen overflow-hidden`}>
{build === "saas" && (
<Script
src="https://rybbit.fossorial.io/api/script.js"
data-site-id="fe1ff2a33287"
strategy="afterInteractive"
/>
)}
<NextIntlClientProvider> <NextIntlClientProvider>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"