Compare commits

...

15 Commits

Author SHA1 Message Date
Owen Schwartz
1f74e1b320 Merge pull request #2776 from fosrl/dev
1.17.0-s.0
2026-04-03 11:39:35 -04:00
Owen
fee780cb81 Add siem to migration 2026-04-03 11:32:02 -04:00
Owen
5056cba85d Add siem to migration 2026-04-03 11:32:02 -04:00
miloschwartz
dab38ff82c use debug log on log stream start 2026-04-03 11:16:56 -04:00
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
8 changed files with 227 additions and 41 deletions

View File

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

View File

@@ -127,7 +127,7 @@ export class LogStreamingManager {
start(): void {
if (this.isRunning) return;
this.isRunning = true;
logger.info("LogStreamingManager: started");
logger.debug("LogStreamingManager: started");
this.schedulePoll(POLL_INTERVAL_MS);
}
@@ -770,4 +770,4 @@ export class LogStreamingManager {
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

@@ -104,6 +104,42 @@ export default async function migration() {
CONSTRAINT "userOrgRoles_userId_orgId_roleId_unique" UNIQUE("userId","orgId","roleId")
);
`);
await db.execute(sql`
CREATE TABLE "eventStreamingCursors" (
"cursorId" serial PRIMARY KEY NOT NULL,
"destinationId" integer NOT NULL,
"logType" varchar(50) NOT NULL,
"lastSentId" bigint DEFAULT 0 NOT NULL,
"lastSentAt" bigint
);
`);
await db.execute(sql`
CREATE TABLE "eventStreamingDestinations" (
"destinationId" serial PRIMARY KEY NOT NULL,
"orgId" varchar(255) NOT NULL,
"sendConnectionLogs" boolean DEFAULT false NOT NULL,
"sendRequestLogs" boolean DEFAULT false NOT NULL,
"sendActionLogs" boolean DEFAULT false NOT NULL,
"sendAccessLogs" boolean DEFAULT false NOT NULL,
"type" varchar(50) NOT NULL,
"config" text NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"createdAt" bigint NOT NULL,
"updatedAt" bigint NOT NULL
);
`);
await db.execute(
sql`ALTER TABLE "eventStreamingCursors" ADD CONSTRAINT "eventStreamingCursors_destinationId_eventStreamingDestinations_destinationId_fk" FOREIGN KEY ("destinationId") REFERENCES "public"."eventStreamingDestinations"("destinationId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "eventStreamingDestinations" ADD CONSTRAINT "eventStreamingDestinations_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`CREATE UNIQUE INDEX "idx_eventStreamingCursors_dest_type" ON "eventStreamingCursors" USING btree ("destinationId","logType");`
);
await db.execute(
sql`ALTER TABLE "userOrgs" DROP CONSTRAINT "userOrgs_roleId_roles_roleId_fk";`
);
@@ -177,8 +213,12 @@ export default async function migration() {
sql`CREATE INDEX "idx_accessAuditLog_siteResourceId" ON "connectionAuditLog" USING btree ("siteResourceId");`
);
await db.execute(sql`ALTER TABLE "userInvites" DROP COLUMN "roleId";`);
await db.execute(sql`ALTER TABLE "siteProvisioningKeys" ADD COLUMN "approveNewSites" boolean DEFAULT true NOT NULL;`);
await db.execute(sql`ALTER TABLE "sites" ADD COLUMN "status" varchar DEFAULT 'approved';`);
await db.execute(
sql`ALTER TABLE "siteProvisioningKeys" ADD COLUMN "approveNewSites" boolean DEFAULT true NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "sites" ADD COLUMN "status" varchar DEFAULT 'approved';`
);
await db.execute(sql`COMMIT`);
console.log("Migrated database");

View File

@@ -76,9 +76,15 @@ export default async function migration() {
`
).run();
db.prepare(`CREATE INDEX 'idx_accessAuditLog_startedAt' ON 'connectionAuditLog' ('startedAt');`).run();
db.prepare(`CREATE INDEX 'idx_accessAuditLog_org_startedAt' ON 'connectionAuditLog' ('orgId','startedAt');`).run();
db.prepare(`CREATE INDEX 'idx_accessAuditLog_siteResourceId' ON 'connectionAuditLog' ('siteResourceId');`).run();
db.prepare(
`CREATE INDEX 'idx_accessAuditLog_startedAt' ON 'connectionAuditLog' ('startedAt');`
).run();
db.prepare(
`CREATE INDEX 'idx_accessAuditLog_org_startedAt' ON 'connectionAuditLog' ('orgId','startedAt');`
).run();
db.prepare(
`CREATE INDEX 'idx_accessAuditLog_siteResourceId' ON 'connectionAuditLog' ('siteResourceId');`
).run();
db.prepare(
`
@@ -168,6 +174,42 @@ export default async function migration() {
);
`
).run();
db.prepare(
`
CREATE TABLE 'eventStreamingCursors' (
'cursorId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'destinationId' integer NOT NULL,
'logType' text NOT NULL,
'lastSentId' integer DEFAULT 0 NOT NULL,
'lastSentAt' integer,
FOREIGN KEY ('destinationId') REFERENCES 'eventStreamingDestinations'('destinationId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE UNIQUE INDEX 'idx_eventStreamingCursors_dest_type' ON 'eventStreamingCursors' ('destinationId','logType');--> statement-breakpoint
`
).run();
db.prepare(
`
CREATE TABLE 'eventStreamingDestinations' (
'destinationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'orgId' text NOT NULL,
'sendConnectionLogs' integer DEFAULT false NOT NULL,
'sendRequestLogs' integer DEFAULT false NOT NULL,
'sendActionLogs' integer DEFAULT false NOT NULL,
'sendAccessLogs' integer DEFAULT false NOT NULL,
'type' text NOT NULL,
'config' text NOT NULL,
'enabled' integer DEFAULT true NOT NULL,
'createdAt' integer NOT NULL,
'updatedAt' integer NOT NULL,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`INSERT INTO '__new_userInvites'("inviteId", "orgId", "email", "expiresAt", "token") SELECT "inviteId", "orgId", "email", "expiresAt", "token" FROM 'userInvites';`
).run();
@@ -191,8 +233,12 @@ export default async function migration() {
`ALTER TABLE 'user' ADD 'marketingEmailConsent' integer DEFAULT false;`
).run();
db.prepare(`ALTER TABLE 'user' ADD 'locale' text;`).run();
db.prepare(`ALTER TABLE 'siteProvisioningKeys' ADD COLUMN 'approveNewSites' integer DEFAULT 1 NOT NULL;`).run();
db.prepare(`ALTER TABLE 'sites' ADD COLUMN 'status' text DEFAULT 'approved';`).run();
db.prepare(
`ALTER TABLE 'siteProvisioningKeys' ADD COLUMN 'approveNewSites' integer DEFAULT 1 NOT NULL;`
).run();
db.prepare(
`ALTER TABLE 'sites' ADD COLUMN 'status' text DEFAULT 'approved';`
).run();
})();
db.pragma("foreign_keys = ON");

View File

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

View File

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

View File

@@ -10,14 +10,14 @@ import {
import { CheckboxWithLabel } from "./ui/checkbox";
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
import { useState } from "react";
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
import { Terminal } from "lucide-react";
import { FaApple, FaCubes, FaDocker, FaLinux, FaWindows } from "react-icons/fa";
import { SiKubernetes, SiNixos } from "react-icons/si";
export type CommandItem = string | { title: string; command: string };
const PLATFORMS = [
"unix",
"linux",
"macos",
"docker",
"kubernetes",
"podman",
@@ -43,7 +43,7 @@ export function NewtSiteInstallCommands({
const t = useTranslations();
const [acceptClients, setAcceptClients] = useState(true);
const [platform, setPlatform] = useState<Platform>("unix");
const [platform, setPlatform] = useState<Platform>("linux");
const [architecture, setArchitecture] = useState(
() => getArchitectures(platform)[0]
);
@@ -54,8 +54,68 @@ export function NewtSiteInstallCommands({
: "";
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
unix: {
All: [
linux: {
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"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
@@ -131,7 +191,7 @@ WantedBy=default.target`
]
},
nixos: {
All: [
Flake: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
}
@@ -172,9 +232,9 @@ WantedBy=default.target`
<OptionSelect<string>
label={
["docker", "podman"].includes(platform)
? t("method")
: t("architecture")
platform === "windows"
? t("architecture")
: t("method")
}
options={getArchitectures(platform).map((arch) => ({
value: arch,
@@ -261,8 +321,10 @@ function getPlatformIcon(platformName: Platform) {
switch (platformName) {
case "windows":
return <FaWindows className="h-4 w-4 mr-2" />;
case "unix":
return <Terminal className="h-4 w-4 mr-2" />;
case "linux":
return <FaLinux className="h-4 w-4 mr-2" />;
case "macos":
return <FaApple className="h-4 w-4 mr-2" />;
case "docker":
return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes":
@@ -272,7 +334,7 @@ function getPlatformIcon(platformName: Platform) {
case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />;
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) {
case "windows":
return "Windows";
case "unix":
return "Unix & macOS";
case "linux":
return "Linux";
case "macos":
return "macOS";
case "docker":
return "Docker";
case "kubernetes":
@@ -291,14 +355,16 @@ function getPlatformName(platformName: Platform) {
case "nixos":
return "NixOS";
default:
return "Unix / macOS";
return "Linux";
}
}
function getArchitectures(platform: Platform) {
switch (platform) {
case "unix":
return ["All"];
case "linux":
return ["Run", "Systemd Service"];
case "macos":
return ["Run"];
case "windows":
return ["x64"];
case "docker":
@@ -308,8 +374,8 @@ function getArchitectures(platform: Platform) {
case "podman":
return ["Podman Quadlet", "Podman Run"];
case "nixos":
return ["All"];
return ["Flake"];
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 userLocale = res.data?.data?.locale;
if (userLocale && locales.includes(userLocale as Locale)) {
// Set the cookie so subsequent requests don't need the API call
(await cookies()).set(COOKIE_NAME, userLocale, {
maxAge: COOKIE_MAX_AGE,
path: "/",
sameSite: "lax"
});
// Try to cache in a cookie so subsequent requests skip the API
// call. cookies().set() is only permitted in Server Actions and
// Route Handlers — not during rendering — so we isolate it so
// that a write failure doesn't prevent the locale from being
// 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;
}
} catch {