mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-03 16:36:38 +00:00
@@ -60,7 +60,7 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
|
||||
|
||||
| <img width=500 /> | Description |
|
||||
|-----------------|--------------|
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. |
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing - no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. |
|
||||
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Provisioning key updated",
|
||||
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
|
||||
"provisioningKeysBannerTitle": "Site Provisioning Keys",
|
||||
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
|
||||
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup - no need to set up separate credentials for each site.",
|
||||
"provisioningKeysBannerButtonText": "Learn More",
|
||||
"pendingSitesBannerTitle": "Pending Sites",
|
||||
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
|
||||
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review.",
|
||||
"pendingSitesBannerButtonText": "Learn More",
|
||||
"apiKeysSettings": "{apiKeyName} Settings",
|
||||
"userTitle": "Manage All Users",
|
||||
@@ -2348,7 +2348,7 @@
|
||||
"description": "Enterprise features, 50 users, 50 sites, and priority support."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Personal use only (free license — no checkout)",
|
||||
"personalUseOnly": "Personal use only (free license - no checkout)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Continue to Checkout"
|
||||
},
|
||||
@@ -2609,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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
|
||||
import { convertTargetsIfNessicary } from "../client/targets";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const handleGetConfigMessage: MessageHandler = async (context) => {
|
||||
const { message, client, sendToClient } = context;
|
||||
@@ -55,7 +56,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
||||
|
||||
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) {
|
||||
logger.warn(
|
||||
`handleGetConfigMessage: Site ${existingSite.siteId} last hole punch is too old, skipping`
|
||||
`Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, newts, sites } from "@server/db";
|
||||
import { db, newts, sites, targetHealthCheck, targets } from "@server/db";
|
||||
import {
|
||||
hasActiveConnections,
|
||||
getClientConfigVersion
|
||||
@@ -78,6 +78,32 @@ export const startNewtOfflineChecker = (): void => {
|
||||
.update(sites)
|
||||
.set({ online: false })
|
||||
.where(eq(sites.siteId, staleSite.siteId));
|
||||
|
||||
const healthChecksOnSite = await db
|
||||
.select()
|
||||
.from(targetHealthCheck)
|
||||
.innerJoin(
|
||||
targets,
|
||||
eq(targets.targetId, targetHealthCheck.targetId)
|
||||
)
|
||||
.innerJoin(sites, eq(sites.siteId, targets.siteId))
|
||||
.where(eq(sites.siteId, staleSite.siteId));
|
||||
|
||||
for (const healthCheck of healthChecksOnSite) {
|
||||
logger.info(
|
||||
`Marking health check ${healthCheck.targetHealthCheck.targetHealthCheckId} offline due to site ${staleSite.siteId} being marked offline`
|
||||
);
|
||||
await db
|
||||
.update(targetHealthCheck)
|
||||
.set({ hcHealth: "unknown" })
|
||||
.where(
|
||||
eq(
|
||||
targetHealthCheck.targetHealthCheckId,
|
||||
healthCheck.targetHealthCheck
|
||||
.targetHealthCheckId
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// this part only effects self hosted. Its not efficient but we dont expect people to have very many wireguard sites
|
||||
@@ -102,7 +128,8 @@ export const startNewtOfflineChecker = (): void => {
|
||||
|
||||
// loop over each one. If its offline and there is a new update then mark it online. If its online and there is no update then mark it offline
|
||||
for (const site of allWireguardSites) {
|
||||
const lastBandwidthUpdate = new Date(site.lastBandwidthUpdate!).getTime() / 1000;
|
||||
const lastBandwidthUpdate =
|
||||
new Date(site.lastBandwidthUpdate!).getTime() / 1000;
|
||||
if (
|
||||
lastBandwidthUpdate < wireguardOfflineThreshold &&
|
||||
site.online
|
||||
|
||||
@@ -20,6 +20,7 @@ import { handleFingerprintInsertion } from "./fingerprintingUtils";
|
||||
import { Alias } from "@server/lib/ip";
|
||||
import { build } from "@server/build";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
logger.info("Handling register olm message!");
|
||||
@@ -274,7 +275,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
||||
logger.warn(
|
||||
"Client last hole punch is too old and we have sites to send; skipping this register"
|
||||
`Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
||||
const [targetCheck] = await db
|
||||
.select({
|
||||
targetId: targets.targetId,
|
||||
siteId: targets.siteId
|
||||
siteId: targets.siteId,
|
||||
hcStatus: targetHealthCheck.hcHealth
|
||||
})
|
||||
.from(targets)
|
||||
.innerJoin(
|
||||
@@ -85,6 +86,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
||||
eq(targets.resourceId, resources.resourceId)
|
||||
)
|
||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||
.innerJoin(targetHealthCheck, eq(targets.targetId, targetHealthCheck.targetId))
|
||||
.where(
|
||||
and(
|
||||
eq(targets.targetId, targetIdNum),
|
||||
@@ -101,6 +103,14 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the status has changed
|
||||
if (targetCheck.hcStatus === healthStatus.status) {
|
||||
logger.debug(
|
||||
`Health status for target ${targetId} is already ${healthStatus.status}, skipping update`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update the target's health status in the database
|
||||
await db
|
||||
.update(targetHealthCheck)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -9,6 +9,8 @@ import DismissableBanner from "@app/components/DismissableBanner";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowRight, Plug } from "lucide-react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type PendingSitesPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -96,6 +98,10 @@ export default async function PendingSitesPage(props: PendingSitesPageProps) {
|
||||
</Button>
|
||||
</Link>
|
||||
</DismissableBanner>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix[TierFeature.SiteProvisioningKeys]}
|
||||
/>
|
||||
|
||||
<PendingSitesTable
|
||||
sites={siteRows}
|
||||
orgId={params.orgId}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -614,6 +614,7 @@ export function InternalResourceForm({
|
||||
<SitesSelector
|
||||
orgId={orgId}
|
||||
selectedSite={selectedSite}
|
||||
filterTypes={["newt"]}
|
||||
onSelectSite={(site) => {
|
||||
setSelectedSite(site);
|
||||
field.onChange(site.siteId);
|
||||
|
||||
@@ -15,6 +15,8 @@ import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { build } from "@server/build";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
ArrowDown01Icon,
|
||||
@@ -63,6 +65,10 @@ export default function PendingSitesTable({
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const canUseSiteProvisioning =
|
||||
isPaidUser(tierMatrix[TierFeature.SiteProvisioningKeys]) &&
|
||||
build !== "oss";
|
||||
|
||||
const booleanSearchFilterSchema = z
|
||||
.enum(["true", "false"])
|
||||
@@ -450,6 +456,7 @@ export default function PendingSitesTable({
|
||||
onSearch={handleSearchChange}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing || isFiltering}
|
||||
refreshButtonDisabled={!canUseSiteProvisioning}
|
||||
rowCount={rowCount}
|
||||
columnVisibility={{
|
||||
niceId: false,
|
||||
|
||||
@@ -311,6 +311,7 @@ export default function SiteProvisioningKeysTable({
|
||||
addButtonDisabled={!canUseSiteProvisioning}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
refreshButtonDisabled={!canUseSiteProvisioning}
|
||||
addButtonText={t("provisioningKeysAdd")}
|
||||
enableColumnVisibility={true}
|
||||
stickyLeftColumn="name"
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,14 @@ export type SitesSelectorProps = {
|
||||
orgId: string;
|
||||
selectedSite?: Selectedsite | null;
|
||||
onSelectSite: (selected: Selectedsite) => void;
|
||||
filterTypes?: string[];
|
||||
};
|
||||
|
||||
export function SitesSelector({
|
||||
orgId,
|
||||
selectedSite,
|
||||
onSelectSite
|
||||
onSelectSite,
|
||||
filterTypes
|
||||
}: SitesSelectorProps) {
|
||||
const t = useTranslations();
|
||||
const [siteSearchQuery, setSiteSearchQuery] = useState("");
|
||||
@@ -45,7 +47,9 @@ export function SitesSelector({
|
||||
|
||||
// always include the selected site in the list of sites shown
|
||||
const sitesShown = useMemo(() => {
|
||||
const allSites: Array<Selectedsite> = [...sites];
|
||||
const allSites: Array<Selectedsite> = filterTypes
|
||||
? sites.filter((s) => filterTypes.includes(s.type))
|
||||
: [...sites];
|
||||
if (
|
||||
debouncedQuery.trim().length === 0 &&
|
||||
selectedSite &&
|
||||
@@ -54,7 +58,7 @@ export function SitesSelector({
|
||||
allSites.unshift(selectedSite);
|
||||
}
|
||||
return allSites;
|
||||
}, [debouncedQuery, sites, selectedSite]);
|
||||
}, [debouncedQuery, sites, selectedSite, filterTypes]);
|
||||
|
||||
return (
|
||||
<Command shouldFilter={false}>
|
||||
|
||||
@@ -69,6 +69,7 @@ type ControlledDataTableProps<TData, TValue> = {
|
||||
onAdd?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
refreshButtonDisabled?: boolean;
|
||||
isNavigatingToAddPage?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
filters?: DataTableFilter[];
|
||||
@@ -91,6 +92,7 @@ export function ControlledDataTable<TData, TValue>({
|
||||
onAdd,
|
||||
onRefresh,
|
||||
isRefreshing,
|
||||
refreshButtonDisabled = false,
|
||||
searchPlaceholder = "Search...",
|
||||
filters,
|
||||
filterDisplayMode = "label",
|
||||
@@ -335,7 +337,7 @@ export function ControlledDataTable<TData, TValue>({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
disabled={isRefreshing || refreshButtonDisabled}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
|
||||
@@ -174,6 +174,7 @@ type DataTableProps<TData, TValue> = {
|
||||
addButtonDisabled?: boolean;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
refreshButtonDisabled?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
searchColumn?: string;
|
||||
defaultSort?: {
|
||||
@@ -207,6 +208,7 @@ export function DataTable<TData, TValue>({
|
||||
addButtonDisabled = false,
|
||||
onRefresh,
|
||||
isRefreshing,
|
||||
refreshButtonDisabled = false,
|
||||
searchPlaceholder = "Search...",
|
||||
searchColumn = "name",
|
||||
defaultSort,
|
||||
@@ -624,7 +626,7 @@ export function DataTable<TData, TValue>({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
disabled={isRefreshing || refreshButtonDisabled}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user