Compare commits

...

11 Commits

Author SHA1 Message Date
Owen
d83fa63af5 Fix to null out the rewrite on the frontend too 2026-04-02 21:58:14 -04:00
Owen
d5837ab718 Fix to use the stored data 2026-04-02 21:57:59 -04:00
Owen
b7ccb92236 Merge branch 'main' into dev 2026-04-02 17:39:25 -04:00
Owen Schwartz
23a151dd45 Merge pull request #2771 from LaurenceJJones/feature/systemd-install-instructions
enhance: Systemd newt instructions
2026-04-02 12:13:44 -04:00
Laurence
122079ddb2 split unix to linux and macos, show method everything other than windows, change nixos all to flake so makes sense under method 2026-04-02 17:05:50 +01:00
Owen Schwartz
1d0b0ae6ec Merge pull request #2770 from fosrl/revert-2766-feature/systemd-install-instructions
Revert "enhance: Systemd newt unit instructions"
2026-04-02 11:43:15 -04:00
Owen Schwartz
0fc1aa9191 Merge pull request #2755 from fosrl/dev
Update go version
2026-03-31 16:04:11 -07:00
Owen Schwartz
ddf417f4ca Merge pull request #2753 from fosrl/dev
Update security
2026-03-31 15:27:47 -07:00
Owen Schwartz
d08be59055 Merge pull request #2752 from fosrl/dev
1.17.0-rc.0
2026-03-31 15:24:25 -07:00
Owen Schwartz
322c136d1f Merge pull request #2748 from jaydeep-pipaliya/fix/empty-targets-toast-message
fix: show contextual toast when saving with no targets
2026-03-31 15:11:12 -07:00
jaydeep-pipaliya
e06f2f47b1 fix: show contextual toast when saving with no targets
Instead of always showing "Settings updated" when saving, show
"Targets cleared" when the target list is empty. This gives the user
accurate feedback without blocking the save action.

Fixes #586
2026-03-31 11:48:56 +05:30
5 changed files with 132 additions and 32 deletions

View File

@@ -624,6 +624,8 @@
"targetErrorInvalidPortDescription": "Please enter a valid port number", "targetErrorInvalidPortDescription": "Please enter a valid port number",
"targetErrorNoSite": "No site selected", "targetErrorNoSite": "No site selected",
"targetErrorNoSiteDescription": "Please select a site for the target", "targetErrorNoSiteDescription": "Please select a site for the target",
"targetTargetsCleared": "Targets cleared",
"targetTargetsClearedDescription": "All targets have been removed from this resource",
"targetCreated": "Target created", "targetCreated": "Target created",
"targetCreatedDescription": "Target has been created successfully", "targetCreatedDescription": "Target has been created successfully",
"targetErrorCreate": "Failed to create target", "targetErrorCreate": "Failed to create target",
@@ -2607,6 +2609,9 @@
"machineClients": "Machine Clients", "machineClients": "Machine Clients",
"install": "Install", "install": "Install",
"run": "Run", "run": "Run",
"envFile": "Environment File",
"serviceFile": "Service File",
"enableAndStart": "Enable and Start",
"clientNameDescription": "The display name of the client that can be changed later.", "clientNameDescription": "The display name of the client that can be changed later.",
"clientAddress": "Client Address (Advanced)", "clientAddress": "Client Address (Advanced)",
"setupFailedToFetchSubnet": "Failed to fetch default subnet", "setupFailedToFetchSubnet": "Failed to fetch default subnet",

View File

@@ -400,7 +400,11 @@ function ProxyResourceTargetsForm({
pathMatchType: row.original.pathMatchType pathMatchType: row.original.pathMatchType
}} }}
onChange={(config) => onChange={(config) =>
updateTarget(row.original.targetId, config) updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
} }
trigger={ trigger={
<Button <Button
@@ -424,7 +428,11 @@ function ProxyResourceTargetsForm({
pathMatchType: row.original.pathMatchType pathMatchType: row.original.pathMatchType
}} }}
onChange={(config) => onChange={(config) =>
updateTarget(row.original.targetId, config) updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
} }
trigger={ trigger={
<Button <Button
@@ -774,8 +782,12 @@ function ProxyResourceTargetsForm({
} }
toast({ toast({
title: t("settingsUpdated"), title: targets.length === 0
description: t("settingsUpdatedDescription") ? t("targetTargetsCleared")
: t("settingsUpdated"),
description: targets.length === 0
? t("targetTargetsClearedDescription")
: t("settingsUpdatedDescription")
}); });
setTargetsToRemove([]); setTargetsToRemove([]);

View File

@@ -776,7 +776,11 @@ export default function Page() {
pathMatchType: row.original.pathMatchType pathMatchType: row.original.pathMatchType
}} }}
onChange={(config) => onChange={(config) =>
updateTarget(row.original.targetId, config) updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
} }
trigger={ trigger={
<Button <Button
@@ -800,7 +804,11 @@ export default function Page() {
pathMatchType: row.original.pathMatchType pathMatchType: row.original.pathMatchType
}} }}
onChange={(config) => onChange={(config) =>
updateTarget(row.original.targetId, config) updateTarget(row.original.targetId,
config.path === null && config.pathMatchType === null
? { ...config, rewritePath: null, rewritePathType: null }
: config
)
} }
trigger={ trigger={
<Button <Button

View File

@@ -10,14 +10,14 @@ import {
import { CheckboxWithLabel } from "./ui/checkbox"; import { CheckboxWithLabel } from "./ui/checkbox";
import { OptionSelect, type OptionSelectOption } from "./OptionSelect"; import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
import { useState } from "react"; import { useState } from "react";
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa"; import { FaApple, FaCubes, FaDocker, FaLinux, FaWindows } from "react-icons/fa";
import { Terminal } from "lucide-react";
import { SiKubernetes, SiNixos } from "react-icons/si"; import { SiKubernetes, SiNixos } from "react-icons/si";
export type CommandItem = string | { title: string; command: string }; export type CommandItem = string | { title: string; command: string };
const PLATFORMS = [ const PLATFORMS = [
"unix", "linux",
"macos",
"docker", "docker",
"kubernetes", "kubernetes",
"podman", "podman",
@@ -43,7 +43,7 @@ export function NewtSiteInstallCommands({
const t = useTranslations(); const t = useTranslations();
const [acceptClients, setAcceptClients] = useState(true); const [acceptClients, setAcceptClients] = useState(true);
const [platform, setPlatform] = useState<Platform>("unix"); const [platform, setPlatform] = useState<Platform>("linux");
const [architecture, setArchitecture] = useState( const [architecture, setArchitecture] = useState(
() => getArchitectures(platform)[0] () => getArchitectures(platform)[0]
); );
@@ -54,8 +54,68 @@ export function NewtSiteInstallCommands({
: ""; : "";
const commandList: Record<Platform, Record<string, CommandItem[]>> = { const commandList: Record<Platform, Record<string, CommandItem[]>> = {
unix: { linux: {
All: [ Run: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
},
{
title: t("run"),
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
}
],
"Systemd Service": [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
},
{
title: t("envFile"),
command: `# Create the directory and environment file
sudo install -d -m 0755 /etc/newt
sudo tee /etc/newt/newt.env > /dev/null << 'EOF'
NEWT_ID=${id}
NEWT_SECRET=${secret}
PANGOLIN_ENDPOINT=${endpoint}${!acceptClients ? `
DISABLE_CLIENTS=true` : ""}
EOF
sudo chmod 600 /etc/newt/newt.env`
},
{
title: t("serviceFile"),
command: `sudo tee /etc/systemd/system/newt.service > /dev/null << 'EOF'
[Unit]
Description=Newt
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=root
Group=root
EnvironmentFile=/etc/newt/newt.env
ExecStart=/usr/local/bin/newt
Restart=always
RestartSec=2
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF`
},
{
title: t("enableAndStart"),
command: `sudo systemctl daemon-reload
sudo systemctl enable --now newt`
}
]
},
macos: {
Run: [
{ {
title: t("install"), title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash` command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
@@ -131,7 +191,7 @@ WantedBy=default.target`
] ]
}, },
nixos: { nixos: {
All: [ Flake: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
] ]
} }
@@ -172,9 +232,9 @@ WantedBy=default.target`
<OptionSelect<string> <OptionSelect<string>
label={ label={
["docker", "podman"].includes(platform) platform === "windows"
? t("method") ? t("architecture")
: t("architecture") : t("method")
} }
options={getArchitectures(platform).map((arch) => ({ options={getArchitectures(platform).map((arch) => ({
value: arch, value: arch,
@@ -261,8 +321,10 @@ function getPlatformIcon(platformName: Platform) {
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 "unix": case "linux":
return <Terminal className="h-4 w-4 mr-2" />; return <FaLinux className="h-4 w-4 mr-2" />;
case "macos":
return <FaApple 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":
@@ -272,7 +334,7 @@ function getPlatformIcon(platformName: Platform) {
case "nixos": case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />; return <SiNixos className="h-4 w-4 mr-2" />;
default: default:
return <Terminal className="h-4 w-4 mr-2" />; return <FaLinux className="h-4 w-4 mr-2" />;
} }
} }
@@ -280,8 +342,10 @@ function getPlatformName(platformName: Platform) {
switch (platformName) { switch (platformName) {
case "windows": case "windows":
return "Windows"; return "Windows";
case "unix": case "linux":
return "Unix & macOS"; return "Linux";
case "macos":
return "macOS";
case "docker": case "docker":
return "Docker"; return "Docker";
case "kubernetes": case "kubernetes":
@@ -291,14 +355,16 @@ function getPlatformName(platformName: Platform) {
case "nixos": case "nixos":
return "NixOS"; return "NixOS";
default: default:
return "Unix / macOS"; return "Linux";
} }
} }
function getArchitectures(platform: Platform) { function getArchitectures(platform: Platform) {
switch (platform) { switch (platform) {
case "unix": case "linux":
return ["All"]; return ["Run", "Systemd Service"];
case "macos":
return ["Run"];
case "windows": case "windows":
return ["x64"]; return ["x64"];
case "docker": case "docker":
@@ -308,8 +374,8 @@ function getArchitectures(platform: Platform) {
case "podman": case "podman":
return ["Podman Quadlet", "Podman Run"]; return ["Podman Quadlet", "Podman Run"];
case "nixos": case "nixos":
return ["All"]; return ["Flake"];
default: default:
return ["x64"]; return ["Run"];
} }
} }

View File

@@ -22,12 +22,21 @@ export async function getUserLocale(): Promise<Locale> {
const res = await internal.get("/user", await authCookieHeader()); const res = await internal.get("/user", await authCookieHeader());
const userLocale = res.data?.data?.locale; const userLocale = res.data?.data?.locale;
if (userLocale && locales.includes(userLocale as Locale)) { if (userLocale && locales.includes(userLocale as Locale)) {
// Set the cookie so subsequent requests don't need the API call // Try to cache in a cookie so subsequent requests skip the API
(await cookies()).set(COOKIE_NAME, userLocale, { // call. cookies().set() is only permitted in Server Actions and
maxAge: COOKIE_MAX_AGE, // Route Handlers — not during rendering — so we isolate it so
path: "/", // that a write failure doesn't prevent the locale from being
sameSite: "lax" // returned for the current request.
}); try {
(await cookies()).set(COOKIE_NAME, userLocale, {
maxAge: COOKIE_MAX_AGE,
path: "/",
sameSite: "lax"
});
} catch {
// Cannot set cookies in this context (e.g. during rendering);
// the correct locale is still returned below.
}
return userLocale as Locale; return userLocale as Locale;
} }
} catch { } catch {