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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ import { NextIntlClientProvider } from "next-intl";
import { getLocale } from "next-intl/server";
import { Toaster } from "@app/components/ui/toaster";
import { build } from "@server/build";
import Script from "next/script";
export const metadata: Metadata = {
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
@@ -62,9 +63,9 @@ export default async function RootLayout({
if (build === "enterprise") {
const licenseStatusRes = await cache(
async () =>
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
)
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
)
)();
licenseStatus = licenseStatusRes.data.data;
} else if (build === "saas") {
@@ -84,6 +85,13 @@ export default async function RootLayout({
return (
<html suppressHydrationWarning lang={locale}>
<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>
<ThemeProvider
attribute="class"