From e0cf0916ddb9a84345f23d59d4b3188fe996ee8d Mon Sep 17 00:00:00 2001 From: Wayne Yao Date: Tue, 8 Jul 2025 23:13:00 +0800 Subject: [PATCH 01/64] Add a few targets to the Makefile to ease local development --- install/Makefile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/install/Makefile b/install/Makefile index 644e4a35..8b65cadd 100644 --- a/install/Makefile +++ b/install/Makefile @@ -1,4 +1,5 @@ all: update-versions go-build-release put-back +dev-all: dev-update-versions dev-build dev-clean go-build-release: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64 @@ -11,6 +12,12 @@ clean: update-versions: @echo "Fetching latest versions..." cp main.go main.go.bak && \ + $(MAKE) dev-update-versions + +put-back: + mv main.go.bak main.go + +dev-update-versions: PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \ GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \ BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \ @@ -20,5 +27,11 @@ update-versions: sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \ echo "Updated main.go with latest versions" -put-back: - mv main.go.bak main.go +dev-build: go-build-release + +dev-clean: + @echo "Restoring version values ..." + sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \ + sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \ + sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go + @echo "Restored version strings in main.go" From 607b168b5649530788156a1dc392c58d43f102d0 Mon Sep 17 00:00:00 2001 From: Wayne Yao Date: Tue, 8 Jul 2025 23:14:23 +0800 Subject: [PATCH 02/64] Use explicity FQDN image path because Podman by default doesn't have unqualified-search, and we don't bother configuring it for users. Being explicit is also a good practice --- install/config/crowdsec/docker-compose.yml | 2 +- install/config/docker-compose.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/install/config/crowdsec/docker-compose.yml b/install/config/crowdsec/docker-compose.yml index 28470d14..17289ef2 100644 --- a/install/config/crowdsec/docker-compose.yml +++ b/install/config/crowdsec/docker-compose.yml @@ -1,6 +1,6 @@ services: crowdsec: - image: crowdsecurity/crowdsec:latest + image: docker.io/crowdsecurity/crowdsec:latest container_name: crowdsec environment: GID: "1000" diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 90349b7a..29c70af0 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -1,7 +1,7 @@ name: pangolin services: pangolin: - image: fosrl/pangolin:{{.PangolinVersion}} + image: docker.io/fosrl/pangolin:{{.PangolinVersion}} container_name: pangolin restart: unless-stopped volumes: @@ -13,7 +13,7 @@ services: retries: 15 {{if .InstallGerbil}} gerbil: - image: fosrl/gerbil:{{.GerbilVersion}} + image: docker.io/fosrl/gerbil:{{.GerbilVersion}} container_name: gerbil restart: unless-stopped depends_on: @@ -35,7 +35,7 @@ services: - 80:80 # Port for traefik because of the network_mode {{end}} traefik: - image: traefik:v3.4.1 + image: docker.io/traefik:v3.4.1 container_name: traefik restart: unless-stopped {{if .InstallGerbil}} From e83e8c2ee4389841408770f5ec583d97e4fa1a5c Mon Sep 17 00:00:00 2001 From: Wayne Yao Date: Tue, 8 Jul 2025 23:14:42 +0800 Subject: [PATCH 03/64] Add podman support to the installer. --- install/crowdsec.go | 12 +-- install/input.txt | 1 + install/main.go | 231 +++++++++++++++++++++++++++++++++----------- 3 files changed, 183 insertions(+), 61 deletions(-) diff --git a/install/crowdsec.go b/install/crowdsec.go index c17bf540..2e388e92 100644 --- a/install/crowdsec.go +++ b/install/crowdsec.go @@ -13,7 +13,7 @@ import ( func installCrowdsec(config Config) error { - if err := stopContainers(); err != nil { + if err := stopContainers(config.InstallationContainerType); err != nil { return fmt.Errorf("failed to stop containers: %v", err) } @@ -72,12 +72,12 @@ func installCrowdsec(config Config) error { os.Exit(1) } - if err := startContainers(); err != nil { + if err := startContainers(config.InstallationContainerType); err != nil { return fmt.Errorf("failed to start containers: %v", err) } // get API key - apiKey, err := GetCrowdSecAPIKey() + apiKey, err := GetCrowdSecAPIKey(config.InstallationContainerType) if err != nil { return fmt.Errorf("failed to get API key: %v", err) } @@ -87,7 +87,7 @@ func installCrowdsec(config Config) error { return fmt.Errorf("failed to replace bouncer key: %v", err) } - if err := restartContainer("traefik"); err != nil { + if err := restartContainer("traefik", config.InstallationContainerType); err != nil { return fmt.Errorf("failed to restart containers: %v", err) } @@ -110,9 +110,9 @@ func checkIsCrowdsecInstalledInCompose() bool { return bytes.Contains(content, []byte("crowdsec:")) } -func GetCrowdSecAPIKey() (string, error) { +func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) { // First, ensure the container is running - if err := waitForContainer("crowdsec"); err != nil { + if err := waitForContainer("crowdsec", containerType); err != nil { return "", fmt.Errorf("waiting for container: %w", err) } diff --git a/install/input.txt b/install/input.txt index 9bca8081..9ecf0d4d 100644 --- a/install/input.txt +++ b/install/input.txt @@ -1,3 +1,4 @@ +docker example.com pangolin.example.com admin@example.com diff --git a/install/main.go b/install/main.go index 38aa6f63..1545640f 100644 --- a/install/main.go +++ b/install/main.go @@ -7,17 +7,17 @@ import ( "fmt" "io" "io/fs" + "math/rand" "os" "os/exec" "os/user" "path/filepath" "runtime" + "strconv" "strings" "syscall" "text/template" "time" - "math/rand" - "strconv" "golang.org/x/term" ) @@ -33,43 +33,99 @@ func loadVersions(config *Config) { var configFiles embed.FS type Config struct { - PangolinVersion string - GerbilVersion string - BadgerVersion string - BaseDomain string - DashboardDomain string - LetsEncryptEmail string - EnableEmail bool - EmailSMTPHost string - EmailSMTPPort int - EmailSMTPUser string - EmailSMTPPass string - EmailNoReply string - InstallGerbil bool - TraefikBouncerKey string - DoCrowdsecInstall bool - Secret string + InstallationContainerType SupportedContainer + PangolinVersion string + GerbilVersion string + BadgerVersion string + BaseDomain string + DashboardDomain string + LetsEncryptEmail string + EnableEmail bool + EmailSMTPHost string + EmailSMTPPort int + EmailSMTPUser string + EmailSMTPPass string + EmailNoReply string + InstallGerbil bool + TraefikBouncerKey string + DoCrowdsecInstall bool + Secret string } +type SupportedContainer string + +const ( + Docker SupportedContainer = "docker" + Podman = "podman" +) + func main() { reader := bufio.NewReader(os.Stdin) + inputContainer := readString(reader, "Would you like to run pangolin as docker or podman container?", "docker") - // check if docker is not installed and the user is root - if !isDockerInstalled() { - if os.Geteuid() != 0 { - fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.") - os.Exit(1) - } + chosenContainer := Docker + if strings.EqualFold(inputContainer, "docker") { + chosenContainer = Docker + } else if strings.EqualFold(inputContainer, "podman") { + chosenContainer = Podman + } else { + fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer) + os.Exit(1) } - // check if the user is in the docker group (linux only) - if !isUserInDockerGroup() { - fmt.Println("You are not in the docker group.") - fmt.Println("The installer will not be able to run docker commands without running it as root.") + if chosenContainer == Podman { + if !isPodmanInstalled() { + fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.") + os.Exit(1) + } + + if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='"); err == nil { + fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.") + fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.") + approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true) + if approved { + if os.Geteuid() != 0 { + fmt.Println("You need to run the installer as root for such a configuration.") + os.Exit(1) + } + + // Podman containers are not able to listen on privileged ports. The official recommendation is to + // container low-range ports as unprivileged ports. + // Linux only. + + if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil { + fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err) + os.Exit(1) + } + } else { + fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.") + } + } else { + fmt.Println("Unprivileged ports have been configured.") + } + + } else if chosenContainer == Docker { + // check if docker is not installed and the user is root + if !isDockerInstalled() { + if os.Geteuid() != 0 { + fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.") + os.Exit(1) + } + } + + // check if the user is in the docker group (linux only) + if !isUserInDockerGroup() { + fmt.Println("You are not in the docker group.") + fmt.Println("The installer will not be able to run docker commands without running it as root.") + os.Exit(1) + } + } else { + // This shouldn't happen unless there's a third container runtime. os.Exit(1) } var config Config + config.InstallationContainerType = chosenContainer // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { @@ -86,7 +142,7 @@ func main() { moveFile("config/docker-compose.yml", "docker-compose.yml") - if !isDockerInstalled() && runtime.GOOS == "linux" { + if !isDockerInstalled() && runtime.GOOS == "linux" && chosenContainer == Docker { if readBool(reader, "Docker is not installed. Would you like to install it?", true) { installDocker() // try to start docker service but ignore errors @@ -115,14 +171,15 @@ func main() { fmt.Println("\n=== Starting installation ===") - if isDockerInstalled() { + if (isDockerInstalled() && chosenContainer == Docker) || + (isPodmanInstalled() && chosenContainer == Podman) { if readBool(reader, "Would you like to install and start the containers?", true) { - if err := pullContainers(); err != nil { + if err := pullContainers(chosenContainer); err != nil { fmt.Println("Error: ", err) return } - if err := startContainers(); err != nil { + if err := startContainers(chosenContainer); err != nil { fmt.Println("Error: ", err) return } @@ -137,6 +194,8 @@ func main() { // check if crowdsec is installed if readBool(reader, "Would you like to install CrowdSec?", false) { fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.") + + // BUG: crowdsec installation will be skipped if the user chooses to install on the first installation. if readBool(reader, "Are you willing to manage CrowdSec?", false) { if config.DashboardDomain == "" { traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") @@ -240,7 +299,7 @@ func collectUserInput(reader *bufio.Reader) Config { config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") - config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") + config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } @@ -456,7 +515,15 @@ func startDockerService() error { } func isDockerInstalled() bool { - cmd := exec.Command("docker", "--version") + return isContainerInstalled("docker") +} + +func isPodmanInstalled() bool { + return isContainerInstalled("podman") && isContainerInstalled("podman-compose") +} + +func isContainerInstalled(container string) bool { + cmd := exec.Command(container, "--version") if err := cmd.Run(); err != nil { return false } @@ -527,52 +594,98 @@ func executeDockerComposeCommandWithArgs(args ...string) error { cmd = exec.Command("docker-compose", args...) } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() } // pullContainers pulls the containers using the appropriate command. -func pullContainers() error { +func pullContainers(containerType SupportedContainer) error { fmt.Println("Pulling the container images...") + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) + } - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { - return fmt.Errorf("failed to pull the containers: %v", err) + return nil } - return nil + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) } // startContainers starts the containers using the appropriate command. -func startContainers() error { +func startContainers(containerType SupportedContainer) error { fmt.Println("Starting containers...") - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { - return fmt.Errorf("failed to start containers: %v", err) + + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { + return fmt.Errorf("failed start containers: %v", err) + } + + return nil } - return nil + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { + return fmt.Errorf("failed to start containers: %v", err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) } // stopContainers stops the containers using the appropriate command. -func stopContainers() error { +func stopContainers(containerType SupportedContainer) error { fmt.Println("Stopping containers...") + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil { + return fmt.Errorf("failed to stop containers: %v", err) + } - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil { - return fmt.Errorf("failed to stop containers: %v", err) + return nil } - return nil + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil { + return fmt.Errorf("failed to stop containers: %v", err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) } // restartContainer restarts a specific container using the appropriate command. -func restartContainer(container string) error { +func restartContainer(container string, containerType SupportedContainer) error { fmt.Println("Restarting containers...") + if containerType == Podman { + if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil { + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + } - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { - return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + return nil } - return nil + if containerType == Docker { + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + } + + return nil + } + + return fmt.Errorf("Unsupported container type: %s", containerType) } func copyFile(src, dst string) error { @@ -600,13 +713,13 @@ func moveFile(src, dst string) error { return os.Remove(src) } -func waitForContainer(containerName string) error { +func waitForContainer(containerName string, containerType SupportedContainer) error { maxAttempts := 30 retryInterval := time.Second * 2 for attempt := 0; attempt < maxAttempts; attempt++ { // Check if container is running - cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName) + cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName) var out bytes.Buffer cmd.Stdout = &out @@ -641,3 +754,11 @@ func generateRandomSecretKey() string { } return string(b) } + +// Run external commands with stdio/stderr attached. +func run(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} From 342675276bd11c980293bf226d500a77dd23db3b Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 13 Jul 2025 15:58:58 -0700 Subject: [PATCH 04/64] Add type & cap --- install/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/main.go b/install/main.go index 1545640f..d3f30f26 100644 --- a/install/main.go +++ b/install/main.go @@ -56,12 +56,12 @@ type SupportedContainer string const ( Docker SupportedContainer = "docker" - Podman = "podman" + Podman SupportedContainer = "podman" ) func main() { reader := bufio.NewReader(os.Stdin) - inputContainer := readString(reader, "Would you like to run pangolin as docker or podman container?", "docker") + inputContainer := readString(reader, "Would you like to run Pangolin as docker or podman container?", "docker") chosenContainer := Docker if strings.EqualFold(inputContainer, "docker") { From bbaea4def050f4fe20d6ab1cd955d3fc2ab62730 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 18 Jul 2025 21:41:58 -0700 Subject: [PATCH 05/64] Handle peer relay dynamically now --- server/routers/olm/handleOlmRelayMessage.ts | 48 ++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index 83a97a41..cefc5b91 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -1,7 +1,7 @@ -import { db } from "@server/db"; +import { db, exitNodes, sites } from "@server/db"; import { MessageHandler } from "../ws"; import { clients, clientSites, Olm } from "@server/db"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { updatePeer } from "../newt/peers"; import logger from "@server/logger"; @@ -30,29 +30,67 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { .limit(1); if (!client) { - logger.warn("Site not found or does not have exit node"); + logger.warn("Client not found"); return; } // make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old if (!client.pubKey) { - logger.warn("Site or client has no endpoint or listen port"); + logger.warn("Client has no endpoint or listen port"); return; } const { siteId } = message.data; + // Get the site + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + + if (!site || !site.exitNodeId) { + logger.warn("Site not found or has no exit node"); + return; + } + + // get the site's exit node + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, site.exitNodeId)) + .limit(1); + + if (!exitNode) { + logger.warn("Exit node not found for site"); + return; + } + await db .update(clientSites) .set({ isRelayed: true }) - .where(eq(clientSites.clientId, olm.clientId)); + .where( + and( + eq(clientSites.clientId, olm.clientId), + eq(clientSites.siteId, siteId) + ) + ); // update the peer on the exit node await updatePeer(siteId, client.pubKey, { endpoint: "" // this removes the endpoint }); + sendToClient(olm.olmId, { + type: "olm/wg/peer/relay", + data: { + siteId: siteId, + endpoint: exitNode.endpoint, + publicKey: exitNode.publicKey + } + }); + return; }; From 86a4656651ea852d1e111f9c4fc98e486c1b2c15 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sat, 19 Jul 2025 22:54:30 -0700 Subject: [PATCH 06/64] fix multi level subdomain conflict bug --- messages/en-US.json | 4 ++-- src/components/DomainPicker.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index ff0ca4e6..0c3dd233 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1196,7 +1196,7 @@ "sidebarExpand": "Expand", "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", - "domainPickerEnterDomain": "Enter your domain", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", "domainPickerDescription": "Enter the full domain of the resource to see available options.", "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", @@ -1206,7 +1206,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Checking availability...", - "domainPickerNoMatchingDomains": "No matching domains found for \"{userInput}\". Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Organization Domains", "domainPickerProvidedDomains": "Provided Domains", "domainPickerSubdomain": "Subdomain: {subdomain}", diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 98ae6b6a..1b96ec8e 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -382,7 +382,7 @@ export default function DomainPicker({ - {t("domainPickerNoMatchingDomains", { userInput })} + {t("domainPickerNoMatchingDomains")} )} From 4443dda0f6979c8d34020ecdfd786a962af04ff4 Mon Sep 17 00:00:00 2001 From: Wayne Yao Date: Mon, 21 Jul 2025 22:48:10 +0800 Subject: [PATCH 07/64] Fix a bug that error check prevents port configuration --- install/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/install/main.go b/install/main.go index d3f30f26..8160f2e9 100644 --- a/install/main.go +++ b/install/main.go @@ -79,7 +79,7 @@ func main() { os.Exit(1) } - if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='"); err == nil { + if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil { fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.") fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.") approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true) @@ -389,7 +389,6 @@ func createConfigFiles(config Config) error { return nil }) - if err != nil { return fmt.Errorf("error walking config files: %v", err) } From d000879c011b5c9746e2d1c1220c0bb1d34cd44a Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 21 Jul 2025 12:42:50 -0700 Subject: [PATCH 08/64] Add config for domains --- server/lib/readConfigFile.ts | 22 ++++++++++++++++++++++ server/routers/domain/createOrgDomain.ts | 7 ++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index f738b986..9ba21aa4 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -231,7 +231,29 @@ export const configSchema = z disable_config_managed_domains: z.boolean().optional(), enable_clients: z.boolean().optional() }) + .optional(), + dns: z + .object({ + nameservers: z + .array(z.string().url()) + .optional() + .default([ + "ns1.fossorial.io", + "ns2.fossorial.io", + ]), + cname_extension: z + .string() + .optional() + .default("fossorial.io"), + }) .optional() + .default({ + nameservers: [ + "ns1.fossorial.io", + "ns2.fossorial.io", + ], + cname_extension: "fossorial.io" + }), }) .refine( (data) => { diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index b401409b..3e84072f 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -11,6 +11,7 @@ import { generateId } from "@server/auth/sessions/app"; import { eq, and } from "drizzle-orm"; import { isValidDomain } from "@server/lib/validators"; import { build } from "@server/build"; +import config from "@server/lib/config"; const paramsSchema = z .object({ @@ -228,15 +229,15 @@ export async function createOrgDomain( // TODO: This needs to be cross region and not hardcoded if (type === "ns") { - nsRecords = ["ns-east.fossorial.io", "ns-west.fossorial.io"]; + nsRecords = config.getRawConfig().dns.nameservers; } else if (type === "cname") { cnameRecords = [ { - value: `${domainId}.cname.fossorial.io`, + value: `${domainId}.${config.getRawConfig().dns.cname_extension}`, baseDomain: baseDomain }, { - value: `_acme-challenge.${domainId}.cname.fossorial.io`, + value: `_acme-challenge.${domainId}.${config.getRawConfig().dns.cname_extension}`, baseDomain: `_acme-challenge.${baseDomain}` } ]; From 9f2710185bfaa130b3e09e46b83845546dbf08b0 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 21 Jul 2025 13:10:34 -0700 Subject: [PATCH 09/64] center toast --- src/components/ui/toast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index c723859c..26510e84 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef< Date: Mon, 21 Jul 2025 14:28:32 -0700 Subject: [PATCH 10/64] allow using password to log in if security keys are available --- server/lib/readConfigFile.ts | 21 +++++------------ server/routers/auth/login.ts | 30 ++++++++++++------------ server/routers/domain/createOrgDomain.ts | 2 +- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 9ba21aa4..b136f61f 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -235,25 +235,16 @@ export const configSchema = z dns: z .object({ nameservers: z - .array(z.string().url()) + .array(z.string().optional().optional()) .optional() - .default([ - "ns1.fossorial.io", - "ns2.fossorial.io", - ]), - cname_extension: z - .string() - .optional() - .default("fossorial.io"), - }) + .default(["ns1.fossorial.io", "ns2.fossorial.io"]), + cname_extension: z.string().optional().default("fossorial.io") + }) .optional() .default({ - nameservers: [ - "ns1.fossorial.io", - "ns2.fossorial.io", - ], + nameservers: ["ns1.fossorial.io", "ns2.fossorial.io"], cname_extension: "fossorial.io" - }), + }) }) .refine( (data) => { diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index cd51e46a..8dad5a42 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -106,21 +106,21 @@ export async function login( ); } - // Check if user has security keys registered - const userSecurityKeys = await db - .select() - .from(securityKeys) - .where(eq(securityKeys.userId, existingUser.userId)); - - if (userSecurityKeys.length > 0) { - return response(res, { - data: { useSecurityKey: true }, - success: true, - error: false, - message: "Security key authentication required", - status: HttpCode.OK - }); - } + // // Check if user has security keys registered + // const userSecurityKeys = await db + // .select() + // .from(securityKeys) + // .where(eq(securityKeys.userId, existingUser.userId)); + // + // if (userSecurityKeys.length > 0) { + // return response(res, { + // data: { useSecurityKey: true }, + // success: true, + // error: false, + // message: "Security key authentication required", + // status: HttpCode.OK + // }); + // } if ( existingUser.twoFactorSetupRequested && diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 3e84072f..08718d44 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -229,7 +229,7 @@ export async function createOrgDomain( // TODO: This needs to be cross region and not hardcoded if (type === "ns") { - nsRecords = config.getRawConfig().dns.nameservers; + nsRecords = config.getRawConfig().dns.nameservers as string[]; } else if (type === "cname") { cnameRecords = [ { From f1bba3b958b22cc1f8e98b0d7302fb61ad2dcf78 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 21 Jul 2025 16:32:02 -0700 Subject: [PATCH 11/64] Fix issues in pg schema --- server/db/pg/schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 39f14598..e256f28d 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -504,8 +504,8 @@ export const clients = pgTable("clients", { name: varchar("name").notNull(), pubKey: varchar("pubKey"), subnet: varchar("subnet").notNull(), - megabytesIn: integer("bytesIn"), - megabytesOut: integer("bytesOut"), + megabytesIn: real("bytesIn"), + megabytesOut: real("bytesOut"), lastBandwidthUpdate: varchar("lastBandwidthUpdate"), lastPing: varchar("lastPing"), type: varchar("type").notNull(), // "olm" @@ -539,7 +539,7 @@ export const olmSessions = pgTable("clientSession", { olmId: varchar("olmId") .notNull() .references(() => olms.olmId, { onDelete: "cascade" }), - expiresAt: integer("expiresAt").notNull() + expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), }); export const userClients = pgTable("userClients", { From 114ce8997f31483602b973505d02a6ac3f5071ac Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 21 Jul 2025 16:56:47 -0700 Subject: [PATCH 12/64] add tos and pp consent --- messages/en-US.json | 8 ++- server/db/pg/schema.ts | 5 +- server/db/sqlite/schema.ts | 2 + server/routers/auth/signup.ts | 25 ++++++--- src/app/auth/signup/SignupForm.tsx | 88 +++++++++++++++++++++++++++--- 5 files changed, 112 insertions(+), 16 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 0c3dd233..ed004d99 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1274,5 +1274,11 @@ "createDomainDnsPropagation": "DNS Propagation", "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", "resourcePortRequired": "Port number is required for non-HTTP resources", - "resourcePortNotAllowed": "Port number should not be set for HTTP resources" + "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + } } diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index e256f28d..77be5f1b 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -5,7 +5,8 @@ import { boolean, integer, bigint, - real + real, + text } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; @@ -135,6 +136,8 @@ export const users = pgTable("user", { twoFactorSecret: varchar("twoFactorSecret"), emailVerified: boolean("emailVerified").notNull().default(false), dateCreated: varchar("dateCreated").notNull(), + termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"), + termsVersion: varchar("termsVersion"), serverAdmin: boolean("serverAdmin").notNull().default(false) }); diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 3e442d07..2c44b593 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -154,6 +154,8 @@ export const users = sqliteTable("user", { .notNull() .default(false), dateCreated: text("dateCreated").notNull(), + termsAcceptedTimestamp: text("termsAcceptedTimestamp"), + termsVersion: text("termsVersion"), serverAdmin: integer("serverAdmin", { mode: "boolean" }) .notNull() .default(false) diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 2508ecfe..09c8db07 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -21,15 +21,14 @@ import { hashPassword } from "@server/auth/password"; import { checkValidInvite } from "@server/auth/checkValidInvite"; import { passwordSchema } from "@server/auth/passwordSchema"; import { UserType } from "@server/types/UserTypes"; +import { build } from "@server/build"; export const signupBodySchema = z.object({ - email: z - .string() - .toLowerCase() - .email(), + email: z.string().toLowerCase().email(), password: passwordSchema, inviteToken: z.string().optional(), - inviteId: z.string().optional() + inviteId: z.string().optional(), + termsAcceptedTimestamp: z.string().nullable().optional() }); export type SignUpBody = z.infer; @@ -54,7 +53,8 @@ export async function signup( ); } - const { email, password, inviteToken, inviteId } = parsedBody.data; + const { email, password, inviteToken, inviteId, termsAcceptedTimestamp } = + parsedBody.data; const passwordHash = await hashPassword(password); const userId = generateId(15); @@ -161,13 +161,24 @@ export async function signup( } } + if (build === "saas" && !termsAcceptedTimestamp) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "You must accept the terms of service and privacy policy" + ) + ); + } + await db.insert(users).values({ userId: userId, type: UserType.Internal, username: email, email: email, passwordHash, - dateCreated: moment().toISOString() + dateCreated: moment().toISOString(), + termsAcceptedTimestamp: termsAcceptedTimestamp || null, + termsVersion: "1" }); // give the user their default permissions: diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index c6ed500b..5494ba10 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -6,6 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, @@ -33,6 +34,7 @@ import Image from "next/image"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; import BrandingLogo from "@app/components/BrandingLogo"; +import { build } from "@server/build"; type SignupFormProps = { redirect?: string; @@ -44,7 +46,19 @@ const formSchema = z .object({ email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, - confirmPassword: passwordSchema + confirmPassword: passwordSchema, + agreeToTerms: z.boolean().refine( + (val) => { + if (build === "saas") { + val === true; + } + return true; + }, + { + message: + "You must agree to the terms of service and privacy policy" + } + ) }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], @@ -64,13 +78,15 @@ export default function SignupForm({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [termsAgreedAt, setTermsAgreedAt] = useState(null); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { email: "", password: "", - confirmPassword: "" + confirmPassword: "", + agreeToTerms: false } }); @@ -85,7 +101,8 @@ export default function SignupForm({ email, password, inviteId, - inviteToken + inviteToken, + termsAcceptedTimestamp: termsAgreedAt }) .catch((e) => { console.error(e); @@ -120,14 +137,23 @@ export default function SignupForm({ return t("authCreateAccount"); } + const handleTermsChange = (checked: boolean) => { + if (checked) { + const isoNow = new Date().toISOString(); + console.log("Terms agreed at:", isoNow); + setTermsAgreedAt(isoNow); + form.setValue("agreeToTerms", true); + } else { + form.setValue("agreeToTerms", false); + setTermsAgreedAt(null); + } + }; + return (
- +

{getSubtitle()}

@@ -180,6 +206,54 @@ export default function SignupForm({ )} /> + {build === "saas" && ( + ( + + + { + field.onChange(checked); + handleTermsChange( + checked as boolean + ); + }} + /> + +
+ + {t("signUpTerms.IAgreeToThe")} + + {t( + "signUpTerms.termsOfService" + )} + + {t("signUpTerms.and")} + + {t( + "signUpTerms.privacyPolicy" + )} + + + +
+
+ )} + /> + )} {error && ( From b54ccbfa2fde0d8bed383ace93db3c4b5f136321 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 21 Jul 2025 17:26:02 -0700 Subject: [PATCH 13/64] fix log in loading button --- src/components/LoginForm.tsx | 183 +++++++++++++++++++++-------------- 1 file changed, 112 insertions(+), 71 deletions(-) diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 153b7eb7..ddd410e2 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -63,7 +63,6 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { const [error, setError] = useState(null); const [loading, setLoading] = useState(false); - const [securityKeyLoading, setSecurityKeyLoading] = useState(false); const hasIdp = idps && idps.length > 0; const [mfaRequested, setMfaRequested] = useState(false); @@ -72,14 +71,12 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { const t = useTranslations(); const formSchema = z.object({ - email: z.string().email({ message: t('emailInvalid') }), - password: z - .string() - .min(8, { message: t('passwordRequirementsChars') }) + email: z.string().email({ message: t("emailInvalid") }), + password: z.string().min(8, { message: t("passwordRequirementsChars") }) }); const mfaSchema = z.object({ - code: z.string().length(6, { message: t('pincodeInvalid') }) + code: z.string().length(6, { message: t("pincodeInvalid") }) }); const form = useForm>({ @@ -99,17 +96,23 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { async function initiateSecurityKeyAuth() { setShowSecurityKeyPrompt(true); - setSecurityKeyLoading(true); + setLoading(true); setError(null); try { // Start WebAuthn authentication without email - const startRes = await api.post("/auth/security-key/authenticate/start", {}); + const startRes = await api.post( + "/auth/security-key/authenticate/start", + {} + ); if (!startRes) { - setError(t('securityKeyAuthError', { - defaultValue: "Failed to start security key authentication" - })); + setError( + t("securityKeyAuthError", { + defaultValue: + "Failed to start security key authentication" + }) + ); return; } @@ -125,7 +128,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { { credential }, { headers: { - 'X-Temp-Session-Id': tempSessionId + "X-Temp-Session-Id": tempSessionId } } ); @@ -136,39 +139,61 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { } } } catch (error: any) { - if (error.name === 'NotAllowedError') { - if (error.message.includes('denied permission')) { - setError(t('securityKeyPermissionDenied', { - defaultValue: "Please allow access to your security key to continue signing in." - })); + if (error.name === "NotAllowedError") { + if (error.message.includes("denied permission")) { + setError( + t("securityKeyPermissionDenied", { + defaultValue: + "Please allow access to your security key to continue signing in." + }) + ); } else { - setError(t('securityKeyRemovedTooQuickly', { - defaultValue: "Please keep your security key connected until the sign-in process completes." - })); + setError( + t("securityKeyRemovedTooQuickly", { + defaultValue: + "Please keep your security key connected until the sign-in process completes." + }) + ); } - } else if (error.name === 'NotSupportedError') { - setError(t('securityKeyNotSupported', { - defaultValue: "Your security key may not be compatible. Please try a different security key." - })); + } else if (error.name === "NotSupportedError") { + setError( + t("securityKeyNotSupported", { + defaultValue: + "Your security key may not be compatible. Please try a different security key." + }) + ); } else { - setError(t('securityKeyUnknownError', { - defaultValue: "There was a problem using your security key. Please try again." - })); + setError( + t("securityKeyUnknownError", { + defaultValue: + "There was a problem using your security key. Please try again." + }) + ); } } } catch (e: any) { if (e.isAxiosError) { - setError(formatAxiosError(e, t('securityKeyAuthError', { - defaultValue: "Failed to authenticate with security key" - }))); + setError( + formatAxiosError( + e, + t("securityKeyAuthError", { + defaultValue: + "Failed to authenticate with security key" + }) + ) + ); } else { console.error(e); - setError(e.message || t('securityKeyAuthError', { - defaultValue: "Failed to authenticate with security key" - })); + setError( + e.message || + t("securityKeyAuthError", { + defaultValue: + "Failed to authenticate with security key" + }) + ); } } finally { - setSecurityKeyLoading(false); + setLoading(false); setShowSecurityKeyPrompt(false); } } @@ -182,11 +207,14 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { setShowSecurityKeyPrompt(false); try { - const res = await api.post>("/auth/login", { - email, - password, - code - }); + const res = await api.post>( + "/auth/login", + { + email, + password, + code + } + ); const data = res.data.data; @@ -212,7 +240,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { } if (data?.twoFactorSetupRequired) { - const setupUrl = `/auth/2fa/setup?email=${encodeURIComponent(email)}${redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''}`; + const setupUrl = `/auth/2fa/setup?email=${encodeURIComponent(email)}${redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""}`; router.push(setupUrl); return; } @@ -222,16 +250,22 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { } } catch (e: any) { if (e.isAxiosError) { - const errorMessage = formatAxiosError(e, t('loginError', { - defaultValue: "Failed to log in" - })); + const errorMessage = formatAxiosError( + e, + t("loginError", { + defaultValue: "Failed to log in" + }) + ); setError(errorMessage); return; } else { console.error(e); - setError(e.message || t('loginError', { - defaultValue: "Failed to log in" - })); + setError( + e.message || + t("loginError", { + defaultValue: "Failed to log in" + }) + ); return; } } finally { @@ -251,7 +285,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { console.log(res); if (!res) { - setError(t('loginError')); + setError(t("loginError")); return; } @@ -268,8 +302,9 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { - {t('securityKeyPrompt', { - defaultValue: "Please verify your identity using your security key. Make sure your security key is connected and ready." + {t("securityKeyPrompt", { + defaultValue: + "Please verify your identity using your security key. Make sure your security key is connected and ready." })} @@ -288,7 +323,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { name="email" render={({ field }) => ( - {t('email')} + {t("email")} @@ -303,7 +338,9 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { name="password" render={({ field }) => ( - {t('password')} + + {t("password")} + - {t('passwordForgot')} + {t("passwordForgot")}
-
@@ -342,11 +379,9 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { {mfaRequested && ( <>
-

- {t('otpAuth')} -

+

{t("otpAuth")}

- {t('otpAuthDescription')} + {t("otpAuthDescription")}

@@ -368,10 +403,16 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { pattern={ REGEXP_ONLY_DIGITS_AND_CHARS } - onChange={(value: string) => { + onChange={( + value: string + ) => { field.onChange(value); - if (value.length === 6) { - mfaForm.handleSubmit(onSubmit)(); + if ( + value.length === 6 + ) { + mfaForm.handleSubmit( + onSubmit + )(); } }} > @@ -422,7 +463,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { loading={loading} disabled={loading} > - {t('otpAuthSubmit')} + {t("otpAuthSubmit")} )} @@ -433,11 +474,11 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { variant="outline" className="w-full" onClick={initiateSecurityKeyAuth} - loading={securityKeyLoading} - disabled={securityKeyLoading || showSecurityKeyPrompt} + loading={loading} + disabled={loading || showSecurityKeyPrompt} > - {t('securityKeyLogin', { + {t("securityKeyLogin", { defaultValue: "Sign in with security key" })} @@ -450,7 +491,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
- {t('idpContinue')} + {t("idpContinue")}
@@ -483,7 +524,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { mfaForm.reset(); }} > - {t('otpAuthBack')} + {t("otpAuthBack")} )} From 5c929badeb209c5aab677c6e662574e4deff5811 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 22 Jul 2025 11:21:39 -0700 Subject: [PATCH 14/64] Send endpoint --- server/routers/olm/handleOlmRegisterMessage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 9f626a7b..cf4ad8b7 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -58,7 +58,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { sendToClient(olm.olmId, { type: "olm/wg/holepunch", data: { - serverPubKey: exitNode.publicKey + serverPubKey: exitNode.publicKey, + endpoint: exitNode.endpoint, } }); } From 52d46f98794650b25b0e7bb2f6019701f45cc09a Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 21 Jul 2025 13:39:39 +0200 Subject: [PATCH 15/64] add nixos option for newt in site creation --- .../[orgId]/settings/sites/create/page.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 454f609e..9b81fc9b 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -42,6 +42,7 @@ import { FaFreebsd, FaWindows } from "react-icons/fa"; +import { SiNixos } from "react-icons/si"; import { Checkbox } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; @@ -74,6 +75,7 @@ type Commands = { windows: Record; docker: Record; podman: Record; + nixos: Record; }; const platforms = [ @@ -82,7 +84,8 @@ const platforms = [ "podman", "mac", "windows", - "freebsd" + "freebsd", + "nixos" ] as const; type Platform = (typeof platforms)[number]; @@ -285,6 +288,14 @@ WantedBy=default.target` "Podman Run": [ `podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}` ] + }, + nixos: { + x86_64: [ + `nix run 'nixpkgs#fosrl-newt' --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + aarch64: [ + `nix run 'nixpkgs#fosrl-newt' --id ${id} --secret ${secret} --endpoint ${endpoint}` + ] } }; setCommands(commands); @@ -304,6 +315,8 @@ WantedBy=default.target` return ["Podman Quadlet", "Podman Run"]; case "freebsd": return ["amd64", "arm64"]; + case "nixos": + return ["x86_64", "aarch64"]; default: return ["x64"]; } @@ -321,6 +334,8 @@ WantedBy=default.target` return "Podman"; case "freebsd": return "FreeBSD"; + case "nixos": + return "NixOS"; default: return "Linux"; } @@ -365,6 +380,8 @@ WantedBy=default.target` return ; case "freebsd": return ; + case "nixos": + return ; default: return ; } From bcc2c59f08b0803c03cda25dc115803c03c97aaf Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:04:55 +0800 Subject: [PATCH 16/64] Add member portal functionality - extracted from feature/member-landing-page --- server/routers/external.ts | 6 + server/routers/resource/getUserResources.ts | 168 +++++ server/routers/resource/index.ts | 3 +- src/app/[orgId]/MemberResourcesPortal.tsx | 732 ++++++++++++++++++++ src/app/[orgId]/page.tsx | 31 +- src/app/navigation.tsx | 24 +- 6 files changed, 938 insertions(+), 26 deletions(-) create mode 100644 server/routers/resource/getUserResources.ts create mode 100644 src/app/[orgId]/MemberResourcesPortal.tsx diff --git a/server/routers/external.ts b/server/routers/external.ts index 6f0b04dc..5bae553e 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -233,6 +233,12 @@ authenticated.get( resource.listResources ); +authenticated.get( + "/org/:orgId/user-resources", + verifyOrgAccess, + resource.getUserResources +); + authenticated.get( "/org/:orgId/domains", verifyOrgAccess, diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts new file mode 100644 index 00000000..681ec4d0 --- /dev/null +++ b/server/routers/resource/getUserResources.ts @@ -0,0 +1,168 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { and, eq, or, inArray } from "drizzle-orm"; +import { + resources, + userResources, + roleResources, + userOrgs, + roles, + resourcePassword, + resourcePincode, + resourceWhitelist, + sites +} from "@server/db"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { response } from "@server/lib/response"; + +export async function getUserResources( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const { orgId } = req.params; + const userId = req.user?.userId; + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + // First get the user's role in the organization + const userOrgResult = await db + .select({ + roleId: userOrgs.roleId + }) + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userId), + eq(userOrgs.orgId, orgId) + ) + ) + .limit(1); + + if (userOrgResult.length === 0) { + return next( + createHttpError(HttpCode.FORBIDDEN, "User not in organization") + ); + } + + const userRoleId = userOrgResult[0].roleId; + + // Get resources accessible through direct assignment or role assignment + const directResourcesQuery = db + .select({ resourceId: userResources.resourceId }) + .from(userResources) + .where(eq(userResources.userId, userId)); + + const roleResourcesQuery = db + .select({ resourceId: roleResources.resourceId }) + .from(roleResources) + .where(eq(roleResources.roleId, userRoleId)); + + const [directResources, roleResourceResults] = await Promise.all([ + directResourcesQuery, + roleResourcesQuery + ]); + + // Combine all accessible resource IDs + const accessibleResourceIds = [ + ...directResources.map(r => r.resourceId), + ...roleResourceResults.map(r => r.resourceId) + ]; + + if (accessibleResourceIds.length === 0) { + return response(res, { + data: { resources: [] }, + success: true, + error: false, + message: "No resources found", + status: HttpCode.OK + }); + } + + // Get resource details for accessible resources + const resourcesData = await db + .select({ + resourceId: resources.resourceId, + name: resources.name, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + enabled: resources.enabled, + sso: resources.sso, + protocol: resources.protocol, + emailWhitelistEnabled: resources.emailWhitelistEnabled, + siteName: sites.name + }) + .from(resources) + .leftJoin(sites, eq(sites.siteId, resources.siteId)) + .where( + and( + inArray(resources.resourceId, accessibleResourceIds), + eq(resources.orgId, orgId), + eq(resources.enabled, true) + ) + ); + + // Check for password, pincode, and whitelist protection for each resource + const resourcesWithAuth = await Promise.all( + resourcesData.map(async (resource) => { + const [passwordCheck, pincodeCheck, whitelistCheck] = await Promise.all([ + db.select().from(resourcePassword).where(eq(resourcePassword.resourceId, resource.resourceId)).limit(1), + db.select().from(resourcePincode).where(eq(resourcePincode.resourceId, resource.resourceId)).limit(1), + db.select().from(resourceWhitelist).where(eq(resourceWhitelist.resourceId, resource.resourceId)).limit(1) + ]); + + const hasPassword = passwordCheck.length > 0; + const hasPincode = pincodeCheck.length > 0; + const hasWhitelist = whitelistCheck.length > 0 || resource.emailWhitelistEnabled; + + return { + resourceId: resource.resourceId, + name: resource.name, + domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, + enabled: resource.enabled, + protected: !!(resource.sso || hasPassword || hasPincode || hasWhitelist), + protocol: resource.protocol, + sso: resource.sso, + password: hasPassword, + pincode: hasPincode, + whitelist: hasWhitelist, + siteName: resource.siteName + }; + }) + ); + + return response(res, { + data: { resources: resourcesWithAuth }, + success: true, + error: false, + message: "User resources retrieved successfully", + status: HttpCode.OK + }); + + } catch (error) { + console.error("Error fetching user resources:", error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Internal server error") + ); + } +} + +export type GetUserResourcesResponse = { + success: boolean; + data: { + resources: Array<{ + resourceId: number; + name: string; + domain: string; + enabled: boolean; + protected: boolean; + protocol: string; + }>; + }; +}; \ No newline at end of file diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 03c9ffbe..f97fcdf4 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -21,4 +21,5 @@ export * from "./getExchangeToken"; export * from "./createResourceRule"; export * from "./deleteResourceRule"; export * from "./listResourceRules"; -export * from "./updateResourceRule"; \ No newline at end of file +export * from "./updateResourceRule"; +export * from "./getUserResources"; \ No newline at end of file diff --git a/src/app/[orgId]/MemberResourcesPortal.tsx b/src/app/[orgId]/MemberResourcesPortal.tsx new file mode 100644 index 00000000..142d5516 --- /dev/null +++ b/src/app/[orgId]/MemberResourcesPortal.tsx @@ -0,0 +1,732 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useTranslations } from "next-intl"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ExternalLink, Globe, ShieldCheck, Search, RefreshCw, AlertCircle, Plus, Shield, ShieldOff, ChevronLeft, ChevronRight, Building2, Key, KeyRound, Fingerprint, AtSign, Copy, InfoIcon } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { GetUserResourcesResponse } from "@server/routers/resource/getUserResources"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { useToast } from "@app/hooks/useToast"; + +// Update Resource type to include site information +type Resource = { + resourceId: number; + name: string; + domain: string; + enabled: boolean; + protected: boolean; + protocol: string; + // Auth method fields + sso?: boolean; + password?: boolean; + pincode?: boolean; + whitelist?: boolean; + // Site information + siteName?: string | null; +}; + +type MemberResourcesPortalProps = { + orgId: string; +}; + +// Favicon component with fallback +const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean }) => { + const [faviconError, setFaviconError] = useState(false); + const [faviconLoaded, setFaviconLoaded] = useState(false); + + // Extract domain for favicon URL + const cleanDomain = domain.replace(/^https?:\/\//, '').split('/')[0]; + const faviconUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=32`; + + const handleFaviconLoad = () => { + setFaviconLoaded(true); + setFaviconError(false); + }; + + const handleFaviconError = () => { + setFaviconError(true); + setFaviconLoaded(false); + }; + + if (faviconError || !enabled) { + return ; + } + + return ( +
+ {!faviconLoaded && ( +
+ )} + {`${cleanDomain} +
+ ); +}; + +// Enhanced status badge component +const StatusBadge = ({ enabled, protected: isProtected, resource }: { enabled: boolean; protected: boolean; resource: Resource }) => { + if (!enabled) { + return ( + + + +
+
+
+
+ +

Resource Disabled

+
+
+
+ ); + } + + if (isProtected) { + return ( + + + +
+ +
+
+ +

Protected Resource

+
+

Authentication Methods:

+
+ {resource.sso && ( +
+
+ +
+ Single Sign-On (SSO) +
+ )} + {resource.password && ( +
+
+ +
+ Password Protected +
+ )} + {resource.pincode && ( +
+
+ +
+ PIN Code +
+ )} + {resource.whitelist && ( +
+
+ +
+ Email Whitelist +
+ )} +
+
+
+
+
+ ); + } + + return ( +
+ +
+ ); +}; + +// Resource Info component +const ResourceInfo = ({ resource }: { resource: Resource }) => { + const hasAuthMethods = resource.sso || resource.password || resource.pincode || resource.whitelist; + + return ( + + + +
+ +
+
+ + {/* Site Information */} + {resource.siteName && ( +
+
Site
+
+ + {resource.siteName} +
+
+ )} + + {/* Authentication Methods */} + {hasAuthMethods && ( +
+
Authentication Methods
+
+ {resource.sso && ( +
+
+ +
+ Single Sign-On (SSO) +
+ )} + {resource.password && ( +
+
+ +
+ Password Protected +
+ )} + {resource.pincode && ( +
+
+ +
+ PIN Code +
+ )} + {resource.whitelist && ( +
+
+ +
+ Email Whitelist +
+ )} +
+
+ )} + + {/* Resource Status - if disabled */} + {!resource.enabled && ( +
+
+ + Resource Disabled +
+
+ )} +
+
+
+ ); +}; + +// Site badge component +const SiteBadge = ({ resource }: { resource: Resource }) => { + if (!resource.siteName) { + return null; + } + + return ( + + + +
+ +
+
+ +

{resource.siteName}

+
+
+
+ ); +}; + +// Pagination component +const PaginationControls = ({ + currentPage, + totalPages, + onPageChange, + totalItems, + itemsPerPage +}: { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + totalItems: number; + itemsPerPage: number; +}) => { + const startItem = (currentPage - 1) * itemsPerPage + 1; + const endItem = Math.min(currentPage * itemsPerPage, totalItems); + + if (totalPages <= 1) return null; + + return ( +
+
+ Showing {startItem}-{endItem} of {totalItems} resources +
+ +
+ + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => { + // Show first page, last page, current page, and 2 pages around current + const showPage = + page === 1 || + page === totalPages || + Math.abs(page - currentPage) <= 1; + + const showEllipsis = + (page === 2 && currentPage > 4) || + (page === totalPages - 1 && currentPage < totalPages - 3); + + if (!showPage && !showEllipsis) return null; + + if (showEllipsis) { + return ( + + ... + + ); + } + + return ( + + ); + })} +
+ + +
+
+ ); +}; + +// Loading skeleton component +const ResourceCardSkeleton = () => ( + + +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); + +export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalProps) { + const t = useTranslations(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { toast } = useToast(); + + const [resources, setResources] = useState([]); + const [filteredResources, setFilteredResources] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState("name-asc"); + const [refreshing, setRefreshing] = useState(false); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 12; // 3x4 grid on desktop + + const fetchUserResources = async (isRefresh = false) => { + try { + if (isRefresh) { + setRefreshing(true); + } else { + setLoading(true); + } + setError(null); + + const response = await api.get( + `/org/${orgId}/user-resources` + ); + + if (response.data.success) { + setResources(response.data.data.resources); + setFilteredResources(response.data.data.resources); + } else { + setError("Failed to load resources"); + } + } catch (err) { + console.error("Error fetching user resources:", err); + setError("Failed to load resources. Please check your connection and try again."); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchUserResources(); + }, [orgId, api]); + + // Filter and sort resources + useEffect(() => { + let filtered = resources.filter(resource => + resource.name.toLowerCase().includes(searchQuery.toLowerCase()) || + resource.domain.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Sort resources + filtered.sort((a, b) => { + switch (sortBy) { + case "name-asc": + return a.name.localeCompare(b.name); + case "name-desc": + return b.name.localeCompare(a.name); + case "domain-asc": + return a.domain.localeCompare(b.domain); + case "domain-desc": + return b.domain.localeCompare(a.domain); + case "status-enabled": + // Enabled first, then protected vs unprotected + if (a.enabled !== b.enabled) return b.enabled ? 1 : -1; + return b.protected ? 1 : -1; + case "status-disabled": + // Disabled first, then unprotected vs protected + if (a.enabled !== b.enabled) return a.enabled ? 1 : -1; + return a.protected ? 1 : -1; + default: + return a.name.localeCompare(b.name); + } + }); + + setFilteredResources(filtered); + + // Reset to first page when search/sort changes + setCurrentPage(1); + }, [resources, searchQuery, sortBy]); + + // Calculate pagination + const totalPages = Math.ceil(filteredResources.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const paginatedResources = filteredResources.slice(startIndex, startIndex + itemsPerPage); + + const handleOpenResource = (resource: Resource) => { + // Open the resource in a new tab + window.open(resource.domain, '_blank'); + }; + + const handleRefresh = () => { + fetchUserResources(true); + }; + + const handleRetry = () => { + fetchUserResources(); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + // Scroll to top when page changes + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + if (loading) { + return ( +
+ + + {/* Search and Sort Controls - Skeleton */} +
+
+
+
+
+
+
+
+ + {/* Loading Skeletons */} +
+ {Array.from({ length: 12 }).map((_, index) => ( + + ))} +
+
+ ); + } + + if (error) { + return ( +
+ + + +
+ +
+

+ Unable to Load Resources +

+

+ {error} +

+ +
+
+
+ ); + } + + return ( +
+ + + {/* Search and Sort Controls with Refresh */} +
+
+ {/* Search */} +
+ setSearchQuery(e.target.value)} + className="w-full pl-8" + /> + +
+ + {/* Sort */} +
+ +
+
+ + {/* Refresh Button */} + +
+ + {/* Resources Content */} + {filteredResources.length === 0 ? ( + /* Enhanced Empty State */ + + +
+ {searchQuery ? ( + + ) : ( + + )} +
+

+ {searchQuery ? "No Resources Found" : "No Resources Available"} +

+

+ {searchQuery + ? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.` + : "You don't have access to any resources yet. Contact your administrator to get access to resources you need." + } +

+
+ {searchQuery ? ( + + ) : ( + + )} +
+
+
+ ) : ( + <> + {/* Resources Grid */} +
+ {paginatedResources.map((resource) => ( + +
+
+
+
+ +
+ + + + + {resource.name} + + + +

{resource.name}

+
+
+
+
+ +
+ +
+
+ +
+ + +
+
+ +
+ +
+
+ ))} +
+ + {/* Pagination Controls */} + + + )} +
+ ); +} \ No newline at end of file diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index d19a6dcc..9a1dda94 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -2,6 +2,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; import { cache } from "react"; import OrganizationLandingCard from "./OrganizationLandingCard"; +import MemberResourcesPortal from "./MemberResourcesPortal"; import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; @@ -9,6 +10,9 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { redirect } from "next/navigation"; import { Layout } from "@app/components/Layout"; import { ListUserOrgsResponse } from "@server/routers/org"; +import { pullEnv } from "@app/lib/pullEnv"; +import EnvProvider from "@app/providers/EnvProvider"; +import { orgLangingNavItems } from "@app/app/navigation"; type OrgPageProps = { params: Promise<{ orgId: string }>; @@ -17,6 +21,7 @@ type OrgPageProps = { export default async function OrgPage(props: OrgPageProps) { const params = await props.params; const orgId = params.orgId; + const env = pullEnv(); const getUser = cache(verifySession); const user = await getUser(); @@ -25,7 +30,6 @@ export default async function OrgPage(props: OrgPageProps) { redirect("/"); } - let redirectToSettings = false; let overview: GetOrgOverviewResponse | undefined; try { const res = await internal.get>( @@ -33,16 +37,14 @@ export default async function OrgPage(props: OrgPageProps) { await authCookieHeader() ); overview = res.data.data; - - if (overview.isAdmin || overview.isOwner) { - redirectToSettings = true; - } } catch (e) {} - if (redirectToSettings) { + // If user is admin or owner, redirect to settings + if (overview?.isAdmin || overview?.isOwner) { redirect(`/${orgId}/settings`); } + // For non-admin users, show the member resources portal let orgs: ListUserOrgsResponse["orgs"] = []; try { const getOrgs = cache(async () => @@ -61,21 +63,8 @@ export default async function OrgPage(props: OrgPageProps) { {overview && ( -
- +
+
)} diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index a18659f2..b8f60abd 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -12,15 +12,31 @@ import { KeyRound, TicketCheck, User, - Globe, - MonitorUp + Globe, // Added from 'dev' branch + MonitorUp // Added from 'dev' branch } from "lucide-react"; -export type SidebarNavSection = { +export type SidebarNavSection = { // Added from 'dev' branch heading: string; items: SidebarNavItem[]; }; +// Merged from 'user-management-and-resources' branch +export const orgLangingNavItems: SidebarNavItem[] = [ + { + title: "sidebarAccount", + href: "/{orgId}", + icon: , + autoExpand: true, + children: [ + { + title: "sidebarResources", + href: "/{orgId}" + } + ] + } +]; + export const orgNavSections = ( enableClients: boolean = true ): SidebarNavSection[] => [ @@ -125,4 +141,4 @@ export const adminNavSections: SidebarNavSection[] = [ : []) ] } -]; +]; \ No newline at end of file From 63494065232f61b479a9f937e55315e9853b44c7 Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:30:20 +0800 Subject: [PATCH 17/64] Removed member resouce sidebar to work with new sidebar. --- src/app/navigation.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index b8f60abd..9901ee2f 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -26,14 +26,7 @@ export const orgLangingNavItems: SidebarNavItem[] = [ { title: "sidebarAccount", href: "/{orgId}", - icon: , - autoExpand: true, - children: [ - { - title: "sidebarResources", - href: "/{orgId}" - } - ] + icon: } ]; From 59cb06acf47417b2a17d1bb5569e3030b380363f Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 24 Jul 2025 14:48:24 -0700 Subject: [PATCH 18/64] Support relaying on register --- .../routers/olm/handleOlmRegisterMessage.ts | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index cf4ad8b7..f504ecd7 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,4 +1,4 @@ -import { db } from "@server/db"; +import { db, ExitNode } from "@server/db"; import { MessageHandler } from "../ws"; import { clients, @@ -28,7 +28,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } const clientId = olm.clientId; - const { publicKey } = message.data; + const { publicKey, relay } = message.data; + + logger.debug(`Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}`); + if (!publicKey) { logger.warn("Public key not provided"); return; @@ -62,6 +65,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { endpoint: exitNode.endpoint, } }); + } if (now - (client.lastHolePunch || 0) > 6) { @@ -85,7 +89,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { await db .update(clientSites) .set({ - isRelayed: false + isRelayed: relay == true }) .where(eq(clientSites.clientId, olm.clientId)); } @@ -98,8 +102,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { .where(eq(clientSites.clientId, client.clientId)); // Prepare an array to store site configurations - const siteConfigurations = []; - + let siteConfigurations = []; + logger.debug(`Found ${sitesData.length} sites for client ${client.clientId}`); // Process each site for (const { sites: site } of sitesData) { if (!site.exitNodeId) { @@ -115,7 +119,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { continue; } - if (site.lastHolePunch && now - site.lastHolePunch > 6) { + if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { logger.warn( `Site ${site.siteId} last hole punch is too old, skipping` ); @@ -143,7 +147,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { await addPeer(site.siteId, { publicKey: publicKey, allowedIps: [`${client.subnet.split('/')[0]}/32`], // we want to only allow from that client - endpoint: client.endpoint + endpoint: relay ? "" : client.endpoint }); } else { logger.warn( @@ -151,10 +155,24 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { ); } + let endpoint = site.endpoint; + if (relay) { + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, site.exitNodeId)) + .limit(1); + if (!exitNode) { + logger.warn(`Exit node not found for site ${site.siteId}`); + continue; + } + endpoint = `${exitNode.endpoint}:21820`; + } + // Add site configuration to the array siteConfigurations.push({ siteId: site.siteId, - endpoint: site.endpoint, + endpoint: endpoint, publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort From 5f75813e84ea05078ca29ea191755d0bd97e2c7f Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 24 Jul 2025 20:47:39 -0700 Subject: [PATCH 19/64] Handle relaying change values in gerbil --- server/routers/gerbil/updateHolePunch.ts | 40 +++---- server/routers/newt/handleGetConfigMessage.ts | 100 +++++++++++++++--- 2 files changed, 104 insertions(+), 36 deletions(-) diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index c48f7551..e99225fe 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -45,7 +45,6 @@ export async function updateHolePunch( const { olmId, newtId, ip, port, timestamp, token } = parsedParams.data; - let currentSiteId: number | undefined; let destinations: PeerDestination[] = []; @@ -174,28 +173,29 @@ export async function updateHolePunch( } // Find all clients that connect to this site - const sitesClientPairs = await db - .select() - .from(clientSites) - .where(eq(clientSites.siteId, newt.siteId)); + // const sitesClientPairs = await db + // .select() + // .from(clientSites) + // .where(eq(clientSites.siteId, newt.siteId)); + // THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING // Get client details for each client - for (const pair of sitesClientPairs) { - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, pair.clientId)); + // for (const pair of sitesClientPairs) { + // const [client] = await db + // .select() + // .from(clients) + // .where(eq(clients.clientId, pair.clientId)); - if (client && client.endpoint) { - const [host, portStr] = client.endpoint.split(':'); - if (host && portStr) { - destinations.push({ - destinationIP: host, - destinationPort: parseInt(portStr, 10) - }); - } - } - } + // if (client && client.endpoint) { + // const [host, portStr] = client.endpoint.split(':'); + // if (host && portStr) { + // destinations.push({ + // destinationIP: host, + // destinationPort: parseInt(portStr, 10) + // }); + // } + // } + // } // If this is a newt/site, also add other sites in the same org // if (updatedSite.orgId) { diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 8d79d4fd..ce887b98 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -2,10 +2,11 @@ import { z } from "zod"; import { MessageHandler } from "../ws"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { db } from "@server/db"; +import { db, ExitNode, exitNodes } from "@server/db"; import { clients, clientSites, Newt, sites } from "@server/db"; import { eq } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; +import axios from "axios"; const inputSchema = z.object({ publicKey: z.string(), @@ -54,7 +55,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { logger.warn("handleGetConfigMessage: Site not found"); return; } - + // we need to wait for hole punch success if (!existingSite.endpoint) { logger.warn(`Site ${existingSite.siteId} has no endpoint, skipping`); @@ -87,6 +88,48 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { return; } + let exitNode: ExitNode | undefined; + if (site.exitNodeId) { + [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, site.exitNodeId)) + .limit(1); + if (exitNode.reachableAt) { + try { + const response = await axios.post( + `${exitNode.reachableAt}/update-proxy-mapping`, + { + oldDestination: { + destinationIP: existingSite.subnet?.split("/")[0], + destinationPort: existingSite.listenPort + }, + newDestination: { + destinationIP: site.subnet?.split("/")[0], + destinationPort: site.listenPort + } + }, + { + headers: { + "Content-Type": "application/json" + } + } + ); + + logger.info("Destinations updated:", { + peer: response.data.status + }); + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}` + ); + } + throw error; + } + } + } + // Get all clients connected to this site const clientsRes = await db .select() @@ -107,33 +150,58 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { if (!client.clients.endpoint) { return false; } - if (!client.clients.online) { - return false; - } - return true; }) .map(async (client) => { // Add or update this peer on the olm if it is connected try { - if (site.endpoint && site.publicKey) { - await updatePeer(client.clients.clientId, { - siteId: site.siteId, - endpoint: site.endpoint, - publicKey: site.publicKey, - serverIP: site.address, - serverPort: site.listenPort - }); + if (!site.publicKey) { + logger.warn( + `Site ${site.siteId} has no public key, skipping` + ); + return null; } + let endpoint = site.endpoint; + if (client.clientSites.isRelayed) { + if (!site.exitNodeId) { + logger.warn( + `Site ${site.siteId} has no exit node, skipping` + ); + return null; + } + + if (!exitNode) { + logger.warn( + `Exit node not found for site ${site.siteId}` + ); + return null; + } + endpoint = `${exitNode.endpoint}:21820`; + } + + if (!endpoint) { + logger.warn( + `Site ${site.siteId} has no endpoint, skipping` + ); + return null; + } + + await updatePeer(client.clients.clientId, { + siteId: site.siteId, + endpoint: endpoint, + publicKey: site.publicKey, + serverIP: site.address, + serverPort: site.listenPort + }); } catch (error) { logger.error( - `Failed to add/update peer ${client.clients.pubKey} to newt ${newt.newtId}: ${error}` + `Failed to add/update peer ${client.clients.pubKey} to olm ${newt.newtId}: ${error}` ); } return { publicKey: client.clients.pubKey!, - allowedIps: [`${client.clients.subnet.split('/')[0]}/32`], // we want to only allow from that client + allowedIps: [`${client.clients.subnet.split("/")[0]}/32`], // we want to only allow from that client endpoint: client.clientSites.isRelayed ? "" : client.clients.endpoint! // if its relayed it should be localhost From 760fe3aca98b1b4b6f0a7eb9a59c00acfe98ecb1 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 24 Jul 2025 21:26:02 -0700 Subject: [PATCH 20/64] Create client component done --- messages/en-US.json | 29 +- .../[orgId]/settings/clients/ClientsTable.tsx | 65 +- .../[orgId]/settings/clients/create/page.tsx | 711 ++++++++++++++++++ 3 files changed, 766 insertions(+), 39 deletions(-) create mode 100644 src/app/[orgId]/settings/clients/create/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index ed004d99..df2d9799 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1280,5 +1280,30 @@ "termsOfService": "terms of service", "and": "and", "privacyPolicy": "privacy policy" - } -} + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "clientNameDescription": "A friendly name for this client", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place." +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/clients/ClientsTable.tsx b/src/app/[orgId]/settings/clients/ClientsTable.tsx index 35ded645..90f04ca8 100644 --- a/src/app/[orgId]/settings/clients/ClientsTable.tsx +++ b/src/app/[orgId]/settings/clients/ClientsTable.tsx @@ -76,42 +76,6 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { }; const columns: ColumnDef[] = [ - { - id: "dots", - cell: ({ row }) => { - const clientRow = row.original; - const router = useRouter(); - - return ( - - - - - - {/* */} - {/* */} - {/* View settings */} - {/* */} - {/* */} - { - setSelectedClient(clientRow); - setIsDeleteModalOpen(true); - }} - > - Delete - - - - ); - } - }, { accessorKey: "name", header: ({ column }) => { @@ -243,6 +207,33 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { const clientRow = row.original; return (
+ + + + + + + {/* */} + {/* */} + {/* View settings */} + {/* */} + {/* */} + { + setSelectedClient(clientRow); + setIsDeleteModalOpen(true); + }} + > + Delete + + + @@ -309,7 +300,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { columns={columns} data={rows} addClient={() => { - setIsCreateModalOpen(true); + router.push(`/${orgId}/settings/clients/create`) }} /> diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx new file mode 100644 index 00000000..850504f5 --- /dev/null +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -0,0 +1,711 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { StrategySelect } from "@app/components/StrategySelect"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import HeaderTitle from "@app/components/SettingsSectionTitle"; +import { z } from "zod"; +import { createElement, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Input } from "@app/components/ui/input"; +import { InfoIcon, Terminal } from "lucide-react"; +import { Button } from "@app/components/ui/button"; +import CopyTextBox from "@app/components/CopyTextBox"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { + FaApple, + FaCubes, + FaDocker, + FaFreebsd, + FaWindows +} from "react-icons/fa"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { + CreateClientBody, + CreateClientResponse, + PickClientDefaultsResponse +} from "@server/routers/client"; +import { ListSitesResponse } from "@server/routers/site"; +import { toast } from "@app/hooks/useToast"; +import { AxiosResponse } from "axios"; +import { useParams, useRouter } from "next/navigation"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; + +import { useTranslations } from "next-intl"; + +type ClientType = "olm"; + +interface TunnelTypeOption { + id: ClientType; + title: string; + description: string; + disabled?: boolean; +} + +type Commands = { + mac: Record; + linux: Record; + windows: Record; +}; + +const platforms = ["linux", "mac", "windows"] as const; + +type Platform = (typeof platforms)[number]; + +export default function Page() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); + const router = useRouter(); + const t = useTranslations(); + + const createClientFormSchema = z.object({ + name: z + .string() + .min(2, { message: t("nameMin", { len: 2 }) }) + .max(30, { message: t("nameMax", { len: 30 }) }), + method: z.enum(["olm"]), + siteIds: z + .array( + z.object({ + id: z.string(), + text: z.string() + }) + ) + .refine((val) => val.length > 0, { + message: t("siteRequired") + }), + subnet: z.string().min(1, { + message: t("subnetRequired") + }) + }); + + type CreateClientFormValues = z.infer; + + const [tunnelTypes, setTunnelTypes] = useState< + ReadonlyArray + >([ + { + id: "olm", + title: t("olmTunnel"), + description: t("olmTunnelDescription"), + disabled: true + } + ]); + + const [loadingPage, setLoadingPage] = useState(true); + const [sites, setSites] = useState([]); + const [activeSitesTagIndex, setActiveSitesTagIndex] = useState< + number | null + >(null); + + const [platform, setPlatform] = useState("linux"); + const [architecture, setArchitecture] = useState("amd64"); + const [commands, setCommands] = useState(null); + + const [olmId, setOlmId] = useState(""); + const [olmSecret, setOlmSecret] = useState(""); + const [olmCommand, setOlmCommand] = useState(""); + + const [createLoading, setCreateLoading] = useState(false); + + const [clientDefaults, setClientDefaults] = + useState(null); + + const hydrateCommands = ( + id: string, + secret: string, + endpoint: string, + version: string + ) => { + const commands = { + mac: { + "Apple Silicon (arm64)": [ + `curl -L -o olm "https://github.com/fosrl/olm/releases/download/${version}/olm_darwin_arm64" && chmod +x ./olm`, + `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + "Intel x64 (amd64)": [ + `curl -L -o olm "https://github.com/fosrl/olm/releases/download/${version}/olm_darwin_amd64" && chmod +x ./olm`, + `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + ] + }, + linux: { + amd64: [ + `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_amd64" && chmod +x ./olm`, + `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + arm64: [ + `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm64" && chmod +x ./olm`, + `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + arm32: [ + `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm32" && chmod +x ./olm`, + `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + arm32v6: [ + `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm32v6" && chmod +x ./olm`, + `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + riscv64: [ + `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_riscv64" && chmod +x ./olm`, + `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + ] + }, + windows: { + x64: [ + `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_amd64.exe"`, + `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}` + ] + } + }; + setCommands(commands); + }; + + const getArchitectures = () => { + switch (platform) { + case "linux": + return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; + case "mac": + return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; + case "windows": + return ["x64"]; + default: + return ["x64"]; + } + }; + + const getPlatformName = (platformName: string) => { + switch (platformName) { + case "windows": + return "Windows"; + case "mac": + return "macOS"; + case "docker": + return "Docker"; + default: + return "Linux"; + } + }; + + const getCommand = () => { + const placeholder = [t("unknownCommand")]; + if (!commands) { + return placeholder; + } + let platformCommands = commands[platform as keyof Commands]; + + if (!platformCommands) { + // get first key + const firstPlatform = Object.keys(commands)[0] as Platform; + platformCommands = commands[firstPlatform as keyof Commands]; + + setPlatform(firstPlatform); + } + + let architectureCommands = platformCommands[architecture]; + if (!architectureCommands) { + // get first key + const firstArchitecture = Object.keys(platformCommands)[0]; + architectureCommands = platformCommands[firstArchitecture]; + + setArchitecture(firstArchitecture); + } + + return architectureCommands || placeholder; + }; + + const getPlatformIcon = (platformName: string) => { + switch (platformName) { + case "windows": + return ; + case "mac": + return ; + case "docker": + return ; + case "podman": + return ; + case "freebsd": + return ; + default: + return ; + } + }; + + const form = useForm({ + resolver: zodResolver(createClientFormSchema), + defaultValues: { + name: "", + method: "olm", + siteIds: [], + subnet: "" + } + }); + + async function onSubmit(data: CreateClientFormValues) { + setCreateLoading(true); + + if (!clientDefaults) { + toast({ + variant: "destructive", + title: t("errorCreatingClient"), + description: t("clientDefaultsNotFound") + }); + setCreateLoading(false); + return; + } + + let payload: CreateClientBody = { + name: data.name, + type: data.method as "olm", + siteIds: data.siteIds.map((site) => parseInt(site.id)), + olmId: clientDefaults.olmId, + secret: clientDefaults.olmSecret, + subnet: data.subnet + }; + + const res = await api + .put< + AxiosResponse + >(`/org/${orgId}/client`, payload) + .catch((e) => { + toast({ + variant: "destructive", + title: t("errorCreatingClient"), + description: formatAxiosError(e) + }); + }); + + if (res && res.status === 201) { + const data = res.data.data; + router.push(`/${orgId}/settings/clients/${data.clientId}`); + } + + setCreateLoading(false); + } + + useEffect(() => { + const load = async () => { + setLoadingPage(true); + + // Fetch available sites + + const res = await api.get>( + `/org/${orgId}/sites/` + ); + const sites = res.data.data.sites.filter( + (s) => s.type === "newt" && s.subnet + ); + setSites( + sites.map((site) => ({ + id: site.siteId.toString(), + text: site.name + })) + ); + + let olmVersion = "latest"; + + try { + const response = await fetch( + `https://api.github.com/repos/fosrl/olm/releases/latest` + ); + if (!response.ok) { + throw new Error( + t("olmErrorFetchReleases", { + err: response.statusText + }) + ); + } + const data = await response.json(); + const latestVersion = data.tag_name; + olmVersion = latestVersion; + } catch (error) { + console.error( + t("olmErrorFetchLatest", { + err: + error instanceof Error + ? error.message + : String(error) + }) + ); + } + + await api + .get(`/org/${orgId}/pick-client-defaults`) + .catch((e) => { + form.setValue("method", "olm"); + }) + .then((res) => { + if (res && res.status === 200) { + const data = res.data.data; + + setClientDefaults(data); + + const olmId = data.olmId; + const olmSecret = data.olmSecret; + const olmCommand = `olm --id ${olmId} --secret ${olmSecret} --endpoint ${env.app.dashboardUrl}`; + + setOlmId(olmId); + setOlmSecret(olmSecret); + setOlmCommand(olmCommand); + + hydrateCommands( + olmId, + olmSecret, + env.app.dashboardUrl, + olmVersion + ); + + if (data.subnet) { + form.setValue("subnet", data.subnet); + } + + setTunnelTypes((prev: any) => { + return prev.map((item: any) => { + return { ...item, disabled: false }; + }); + }); + } + }); + + setLoadingPage(false); + }; + + load(); + }, []); + + return ( + <> +
+ + +
+ + {!loadingPage && ( +
+ + + + + {t("clientInformation")} + + + + + + + ( + + + {t("name")} + + + + + + + {t("clientNameDescription")} + + + )} + /> + + ( + + + {t("address")} + + + + + + + {t("addressDescription")} + + + )} + /> + + ( + + + {t("sites")} + + { + form.setValue( + "siteIds", + olmags as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + sites + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + {t("sitesDescription")} + + + + )} + /> + + + + + + + {form.watch("method") === "olm" && ( + <> + + + + {t("clientOlmCredentials")} + + + {t("clientOlmCredentialsDescription")} + + + + + + + {t("olmEndpoint")} + + + + + + + + {t("olmId")} + + + + + + + + {t("olmSecretKey")} + + + + + + + + + + + {t("clientCredentialsSave")} + + + {t( + "clientCredentialsSaveDescription" + )} + + + + + + + + {t("clientInstallOlm")} + + + {t("clientInstallOlmDescription")} + + + +
+

+ {t("operatingSystem")} +

+
+ {platforms.map((os) => ( + + ))} +
+
+ +
+

+ {["docker", "podman"].includes( + platform + ) + ? t("method") + : t("architecture")} +

+
+ {getArchitectures().map( + (arch) => ( + + ) + )} +
+
+

+ {t("commands")} +

+
+ +
+
+
+
+
+ + )} +
+ +
+ + +
+
+ )} + + ); +} \ No newline at end of file From 1466788f77bdbff2468cfa4f81404d7f407a74d7 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 24 Jul 2025 21:42:44 -0700 Subject: [PATCH 21/64] Clients ui done --- messages/en-US.json | 13 +- .../[orgId]/settings/clients/ClientsTable.tsx | 10 - .../settings/clients/CreateClientsForm.tsx | 349 ------------------ .../settings/clients/CreateClientsModal.tsx | 80 ---- .../clients/[clientId]/ClientInfoCard.tsx | 12 +- .../clients/[clientId]/general/page.tsx | 30 +- .../[orgId]/settings/clients/create/page.tsx | 6 +- .../[orgId]/settings/sites/create/page.tsx | 5 - 8 files changed, 31 insertions(+), 474 deletions(-) delete mode 100644 src/app/[orgId]/settings/clients/CreateClientsForm.tsx delete mode 100644 src/app/[orgId]/settings/clients/CreateClientsModal.tsx diff --git a/messages/en-US.json b/messages/en-US.json index df2d9799..a9051087 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Error creating site", "siteErrorCreateKeyPair": "Key pair or site defaults not found", "siteErrorCreateDefaults": "Site defaults not found", - "siteNameDescription": "This is the display name for the site.", "method": "Method", "siteMethodDescription": "This is how you will expose connections.", "siteLearnNewt": "Learn how to install Newt on your system", @@ -1291,7 +1290,6 @@ "seeAllClients": "See All Clients", "clientInformation": "Client Information", "clientNamePlaceholder": "Client name", - "clientNameDescription": "A friendly name for this client", "address": "Address", "subnetPlaceholder": "Subnet", "addressDescription": "The address that this client will use for connectivity", @@ -1305,5 +1303,14 @@ "olmId": "Olm ID", "olmSecretKey": "Olm Secret Key", "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place." + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release." } \ No newline at end of file diff --git a/src/app/[orgId]/settings/clients/ClientsTable.tsx b/src/app/[orgId]/settings/clients/ClientsTable.tsx index 90f04ca8..89766dfc 100644 --- a/src/app/[orgId]/settings/clients/ClientsTable.tsx +++ b/src/app/[orgId]/settings/clients/ClientsTable.tsx @@ -25,7 +25,6 @@ import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import CreateClientFormModal from "./CreateClientsModal"; export type ClientRow = { id: number; @@ -250,15 +249,6 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { return ( <> - { - setRows([val, ...rows]); - }} - orgId={orgId} - /> - {selectedClient && ( val.length > 0, { - message: "At least one site is required." - }), - subnet: z.string().min(1, { - message: "Subnet is required." - }) -}); - -type CreateClientFormValues = z.infer; - -const defaultValues: Partial = { - name: "", - siteIds: [], - subnet: "" -}; - -type CreateClientFormProps = { - onCreate?: (client: ClientRow) => void; - setLoading?: (loading: boolean) => void; - setChecked?: (checked: boolean) => void; - orgId: string; -}; - -export default function CreateClientForm({ - onCreate, - setLoading, - setChecked, - orgId -}: CreateClientFormProps) { - const api = createApiClient(useEnvContext()); - const { env } = useEnvContext(); - - const [sites, setSites] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [isChecked, setIsChecked] = useState(false); - const [clientDefaults, setClientDefaults] = - useState(null); - const [olmCommand, setOlmCommand] = useState(null); - const [selectedSites, setSelectedSites] = useState< - Array<{ id: number; name: string }> - >([]); - const [activeSitesTagIndex, setActiveSitesTagIndex] = useState< - number | null - >(null); - - const handleCheckboxChange = (checked: boolean) => { - setIsChecked(checked); - if (setChecked) { - setChecked(checked); - } - }; - - const form = useForm({ - resolver: zodResolver(createClientFormSchema), - defaultValues - }); - - useEffect(() => { - if (!open) return; - - // reset all values - setLoading?.(false); - setIsLoading(false); - form.reset(); - setChecked?.(false); - setClientDefaults(null); - setSelectedSites([]); - - const fetchSites = async () => { - const res = await api.get>( - `/org/${orgId}/sites/` - ); - const sites = res.data.data.sites.filter( - (s) => s.type === "newt" && s.subnet - ); - setSites( - sites.map((site) => ({ - id: site.siteId.toString(), - text: site.name - })) - ); - }; - - const fetchDefaults = async () => { - api.get(`/org/${orgId}/pick-client-defaults`) - .catch((e) => { - toast({ - variant: "destructive", - title: `Error fetching client defaults`, - description: formatAxiosError(e) - }); - }) - .then((res) => { - if (res && res.status === 200) { - const data = res.data.data; - setClientDefaults(data); - const olmConfig = `olm --id ${data?.olmId} --secret ${data?.olmSecret} --endpoint ${env.app.dashboardUrl}`; - setOlmCommand(olmConfig); - - // Set the subnet value from client defaults - if (data?.subnet) { - form.setValue("subnet", data.subnet); - } - } - }); - }; - fetchSites(); - fetchDefaults(); - }, [open]); - - async function onSubmit(data: CreateClientFormValues) { - setLoading?.(true); - setIsLoading(true); - - if (!clientDefaults) { - toast({ - variant: "destructive", - title: "Error creating client", - description: "Client defaults not found" - }); - setLoading?.(false); - setIsLoading(false); - return; - } - - const payload = { - name: data.name, - siteIds: data.siteIds.map((site) => parseInt(site.id)), - olmId: clientDefaults.olmId, - secret: clientDefaults.olmSecret, - subnet: data.subnet, - type: "olm" - } as CreateClientBody; - - const res = await api - .put< - AxiosResponse - >(`/org/${orgId}/client`, payload) - .catch((e) => { - toast({ - variant: "destructive", - title: "Error creating client", - description: formatAxiosError(e) - }); - }); - - if (res && res.status === 201) { - const data = res.data.data; - - onCreate?.({ - name: data.name, - id: data.clientId, - subnet: data.subnet, - mbIn: "0 MB", - mbOut: "0 MB", - orgId: orgId as string, - online: false - }); - } - - setLoading?.(false); - setIsLoading(false); - } - - return ( -
-
- - ( - - Name - - - - - - )} - /> - - ( - - Address - - - - - The address that this client will use for - connectivity. - - - - )} - /> - - ( - - Sites - { - form.setValue( - "siteIds", - newTags as [Tag, ...Tag[]] - ); - }} - enableAutocomplete={true} - autocompleteOptions={sites} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={true} - sortTags={true} - /> - - The client will have connectivity to the - selected sites. The sites must be configured - to accept client connections. - - - - )} - /> - - {olmCommand && ( -
-
-
- -
-
- - You will only be able to see the configuration - once. - -
- )} - -
- - -
- - -
- ); -} diff --git a/src/app/[orgId]/settings/clients/CreateClientsModal.tsx b/src/app/[orgId]/settings/clients/CreateClientsModal.tsx deleted file mode 100644 index a8921cb1..00000000 --- a/src/app/[orgId]/settings/clients/CreateClientsModal.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client"; - -import { Button } from "@app/components/ui/button"; -import { useState } from "react"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import CreateClientForm from "./CreateClientsForm"; -import { ClientRow } from "./ClientsTable"; - -type CreateClientFormProps = { - open: boolean; - setOpen: (open: boolean) => void; - onCreate?: (client: ClientRow) => void; - orgId: string; -}; - -export default function CreateClientFormModal({ - open, - setOpen, - onCreate, - orgId -}: CreateClientFormProps) { - const [loading, setLoading] = useState(false); - const [isChecked, setIsChecked] = useState(false); - - return ( - <> - { - setOpen(val); - setLoading(false); - }} - > - - - Create Client - - Create a new client to connect to your sites - - - -
- setLoading(val)} - setChecked={(val) => setIsChecked(val)} - onCreate={onCreate} - orgId={orgId} - /> -
-
- - - - - - -
-
- - ); -} diff --git a/src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx b/src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx index 7117b4d5..ec8ecacf 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx @@ -9,38 +9,40 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; +import { useTranslations } from "next-intl"; type ClientInfoCardProps = {}; export default function SiteInfoCard({}: ClientInfoCardProps) { const { client, updateClient } = useClientContext(); + const t = useTranslations(); return ( - Client Information + {t("clientInformation")} <> - Status + {t("status")} {client.online ? (
- Online + {t("online")}
) : (
- Offline + {t("offline")}
)}
- Address + {t("address")} {client.subnet.split("/")[0]} diff --git a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx index e02e3aaa..27d708a4 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx @@ -34,6 +34,7 @@ import { useEffect, useState } from "react"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { AxiosResponse } from "axios"; import { ListSitesResponse } from "@server/routers/site"; +import { useTranslations } from "next-intl"; const GeneralFormSchema = z.object({ name: z.string().nonempty("Name is required"), @@ -48,6 +49,7 @@ const GeneralFormSchema = z.object({ type GeneralFormValues = z.infer; export default function GeneralPage() { + const t = useTranslations(); const { client, updateClient } = useClientContext(); const api = createApiClient(useEnvContext()); const [loading, setLoading] = useState(false); @@ -119,18 +121,18 @@ export default function GeneralPage() { updateClient({ name: data.name }); toast({ - title: "Client updated", - description: "The client has been updated." + title: t("clientUpdated"), + description: t("clientUpdatedDescription") }); router.refresh(); } catch (e) { toast({ variant: "destructive", - title: "Failed to update client", + title: t("clientUpdateFailed"), description: formatAxiosError( e, - "An error occurred while updating the client." + t("clientUpdateError") ) }); } finally { @@ -143,10 +145,10 @@ export default function GeneralPage() { - General Settings + {t("generalSettings")} - Configure the general settings for this client + {t("generalSettingsDescription")} @@ -163,15 +165,11 @@ export default function GeneralPage() { name="name" render={({ field }) => ( - Name + {t("name")} - - This is the display name of the - client. - )} /> @@ -181,12 +179,12 @@ export default function GeneralPage() { name="siteIds" render={(field) => ( - Sites + {t("sites")} { @@ -202,9 +200,7 @@ export default function GeneralPage() { sortTags={true} /> - The client will have connectivity to the - selected sites. The sites must be configured - to accept client connections. + {t("sitesDescription")} @@ -222,7 +218,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - Save Settings + {t("saveSettings")} diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 850504f5..88d2bef2 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -100,7 +100,7 @@ export default function Page() { .refine((val) => val.length > 0, { message: t("siteRequired") }), - subnet: z.string().min(1, { + subnet: z.string().ip().min(1, { message: t("subnetRequired") }) }); @@ -442,14 +442,10 @@ export default function Page() { - - {t("clientNameDescription")} - )} /> diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 454f609e..9ea254b1 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -587,11 +587,6 @@ WantedBy=default.target` /> - - {t( - "siteNameDescription" - )} - )} /> From 15adfcca8cf745d22bc4785144e99b64b16cc3ec Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 24 Jul 2025 22:01:22 -0700 Subject: [PATCH 22/64] Add remote subnets to ui --- messages/en-US.json | 5 +- server/db/pg/schema.ts | 13 ++-- server/db/sqlite/schema.ts | 3 +- server/routers/site/updateSite.ts | 19 ++++++ .../settings/sites/[niceId]/general/page.tsx | 66 +++++++++++++++++-- 5 files changed, 92 insertions(+), 14 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index a9051087..8e78c3d2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1312,5 +1312,8 @@ "sitesFetchFailed": "Failed to fetch sites", "sitesFetchError": "An error occurred while fetching sites.", "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release." + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24." } \ No newline at end of file diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 77be5f1b..d774a985 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -59,7 +59,8 @@ export const sites = pgTable("sites", { publicKey: varchar("publicKey"), lastHolePunch: bigint("lastHolePunch", { mode: "number" }), listenPort: integer("listenPort"), - dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true) + dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true), + remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access }); export const resources = pgTable("resources", { @@ -542,7 +543,7 @@ export const olmSessions = pgTable("clientSession", { olmId: varchar("olmId") .notNull() .references(() => olms.olmId, { onDelete: "cascade" }), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), + expiresAt: bigint("expiresAt", { mode: "number" }).notNull() }); export const userClients = pgTable("userClients", { @@ -565,9 +566,11 @@ export const roleClients = pgTable("roleClients", { export const securityKeys = pgTable("webauthnCredentials", { credentialId: varchar("credentialId").primaryKey(), - userId: varchar("userId").notNull().references(() => users.userId, { - onDelete: "cascade" - }), + userId: varchar("userId") + .notNull() + .references(() => users.userId, { + onDelete: "cascade" + }), publicKey: varchar("publicKey").notNull(), signCount: integer("signCount").notNull(), transports: varchar("transports"), diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 2c44b593..d372856d 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -65,7 +65,8 @@ export const sites = sqliteTable("sites", { listenPort: integer("listenPort"), dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) .notNull() - .default(true) + .default(true), + remoteSubnets: text("remoteSubnets"), // comma-separated list of subnets that this site can access }); export const resources = sqliteTable("resources", { diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index a5a5f7c0..e3724f36 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -9,6 +9,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { isValidCIDR } from "@server/lib/validators"; const updateSiteParamsSchema = z .object({ @@ -20,6 +21,9 @@ const updateSiteBodySchema = z .object({ name: z.string().min(1).max(255).optional(), dockerSocketEnabled: z.boolean().optional(), + remoteSubnets: z + .string() + .optional() // subdomain: z // .string() // .min(1) @@ -85,6 +89,21 @@ export async function updateSite( const { siteId } = parsedParams.data; const updateData = parsedBody.data; + // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs + if (updateData.remoteSubnets) { + const subnets = updateData.remoteSubnets.split(",").map((s) => s.trim()); + for (const subnet of subnets) { + if (!isValidCIDR(subnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Invalid CIDR format: ${subnet}` + ) + ); + } + } + } + const updatedSite = await db .update(sites) .set(updateData) diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index ba1f877c..1581d961 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -33,10 +33,17 @@ import { useState } from "react"; import { SwitchInput } from "@app/components/SwitchInput"; import { useTranslations } from "next-intl"; import Link from "next/link"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; const GeneralFormSchema = z.object({ name: z.string().nonempty("Name is required"), - dockerSocketEnabled: z.boolean().optional() + dockerSocketEnabled: z.boolean().optional(), + remoteSubnets: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ).optional() }); type GeneralFormValues = z.infer; @@ -44,9 +51,11 @@ type GeneralFormValues = z.infer; export default function GeneralPage() { const { site, updateSite } = useSiteContext(); + const { env } = useEnvContext(); const api = createApiClient(useEnvContext()); const [loading, setLoading] = useState(false); + const [activeCidrTagIndex, setActiveCidrTagIndex] = useState(null); const router = useRouter(); const t = useTranslations(); @@ -55,7 +64,13 @@ export default function GeneralPage() { resolver: zodResolver(GeneralFormSchema), defaultValues: { name: site?.name, - dockerSocketEnabled: site?.dockerSocketEnabled ?? false + dockerSocketEnabled: site?.dockerSocketEnabled ?? false, + remoteSubnets: site?.remoteSubnets + ? site.remoteSubnets.split(',').map((subnet, index) => ({ + id: subnet.trim(), + text: subnet.trim() + })) + : [] }, mode: "onChange" }); @@ -66,7 +81,8 @@ export default function GeneralPage() { await api .post(`/site/${site?.siteId}`, { name: data.name, - dockerSocketEnabled: data.dockerSocketEnabled + dockerSocketEnabled: data.dockerSocketEnabled, + remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || '' }) .catch((e) => { toast({ @@ -81,7 +97,8 @@ export default function GeneralPage() { updateSite({ name: data.name, - dockerSocketEnabled: data.dockerSocketEnabled + dockerSocketEnabled: data.dockerSocketEnabled, + remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || '' }); toast({ @@ -124,12 +141,47 @@ export default function GeneralPage() { - - {t("siteNameDescription")} - )} /> + + ( + + {t("remoteSubnets")} + + { + form.setValue( + "remoteSubnets", + newSubnets as Tag[] + ); + }} + validateTag={(tag) => { + // Basic CIDR validation regex + const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/; + return cidrRegex.test(tag); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("remoteSubnetsDescription")} + + + + )} + /> + {site && site.type === "newt" && ( Date: Sat, 26 Jul 2025 18:47:50 +1000 Subject: [PATCH 23/64] add an environment variable for the smtp_pass config option The password for secure authentication may be sensitive, so it is best to not leave it lying around in a config file. This commit introduces the EMAIL_SMTP_PASS environment variable, which can be set to configure the SMTP password without writing it to the configuration file. Signed-off-by: Fernando Rodrigues --- server/lib/readConfigFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index f738b986..da1e1649 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -213,7 +213,7 @@ export const configSchema = z smtp_host: z.string().optional(), smtp_port: portSchema.optional(), smtp_user: z.string().optional(), - smtp_pass: z.string().optional(), + smtp_pass: z.string().optional().transform(getEnvOrYaml("EMAIL_SMTP_PASS")), smtp_secure: z.boolean().optional(), smtp_tls_reject_unauthorized: z.boolean().optional(), no_reply: z.string().email().optional() From 9e87c42d0cabc241ee13b3769ca75ce3d7a1259f Mon Sep 17 00:00:00 2001 From: Fernando Rodrigues Date: Sun, 27 Jul 2025 13:07:00 +1000 Subject: [PATCH 24/64] add shebangs to migration and server scripts In NixOS, we wrap these files in a bash script to allow users to just run them as normal executables, instead of calling them as arguments to Node.JS. In our build scripts, we just add the shebang after the files have been compiled, but adding it upstream will allow all Pangolin users to just run ./server.mjs to start their Pangolin instances. Signed-off-by: Fernando Rodrigues --- server/index.ts | 1 + server/setup/migrationsPg.ts | 1 + server/setup/migrationsSqlite.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/server/index.ts b/server/index.ts index 55b34543..d3f90281 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,3 +1,4 @@ +#! /usr/bin/env node import "./extendZod.ts"; import { runSetupFunctions } from "./setup"; diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index d5e2f46d..6996999c 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -1,3 +1,4 @@ +#! /usr/bin/env node import { migrate } from "drizzle-orm/node-postgres/migrator"; import { db } from "../db/pg"; import semver from "semver"; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 68da0a27..9fd5a470 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -1,3 +1,4 @@ +#! /usr/bin/env node import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { db, exists } from "../db/sqlite"; import path from "path"; From 28f8b05dbcc847036c20e61f09279b72117c5129 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 27 Jul 2025 10:21:27 -0700 Subject: [PATCH 25/64] Basic clients working --- messages/en-US.json | 7 +- server/db/pg/schema.ts | 3 +- server/db/sqlite/schema.ts | 3 +- server/routers/client/updateClient.ts | 3 +- server/routers/newt/handleGetConfigMessage.ts | 100 ++++++++++++++- server/routers/newt/targets.ts | 42 +++++-- .../routers/olm/handleOlmRegisterMessage.ts | 15 +-- server/routers/olm/peers.ts | 8 +- server/routers/resource/createResource.ts | 10 +- server/routers/resource/deleteResource.ts | 3 +- server/routers/resource/transferResource.ts | 6 +- server/routers/resource/updateResource.ts | 3 +- server/routers/target/createTarget.ts | 2 +- server/routers/target/deleteTarget.ts | 2 +- server/routers/target/updateTarget.ts | 2 +- server/routers/traefik/getTraefikConfig.ts | 7 +- server/setup/scriptsSqlite/1.8.0.ts | 29 +++++ src/app/[orgId]/settings/clients/page.tsx | 2 +- .../[resourceId]/ResourceInfoBox.tsx | 37 ++++-- .../resources/[resourceId]/general/page.tsx | 118 +++++++++++++----- .../settings/resources/create/page.tsx | 72 +++++++++-- 21 files changed, 387 insertions(+), 87 deletions(-) create mode 100644 server/setup/scriptsSqlite/1.8.0.ts diff --git a/messages/en-US.json b/messages/en-US.json index 8e78c3d2..4ff1b866 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "All Users", "sidebarIdentityProviders": "Identity Providers", "sidebarLicense": "License", - "sidebarClients": "Clients", + "sidebarClients": "Clients (beta)", "sidebarDomains": "Domains", "enableDockerSocket": "Enable Docker Socket", "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", @@ -1315,5 +1315,8 @@ "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24." + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } \ No newline at end of file diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index d774a985..5709c9f8 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -94,7 +94,8 @@ export const resources = pgTable("resources", { enabled: boolean("enabled").notNull().default(true), stickySession: boolean("stickySession").notNull().default(false), tlsServerName: varchar("tlsServerName"), - setHostHeader: varchar("setHostHeader") + setHostHeader: varchar("setHostHeader"), + enableProxy: boolean("enableProxy").notNull().default(true), }); export const targets = pgTable("targets", { diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index d372856d..974faa67 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -106,7 +106,8 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), tlsServerName: text("tlsServerName"), - setHostHeader: text("setHostHeader") + setHostHeader: text("setHostHeader"), + enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), }); export const targets = sqliteTable("targets", { diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 87bb3c47..73c67d53 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -147,7 +147,8 @@ export async function updateClient( endpoint: site.endpoint, publicKey: site.publicKey, serverIP: site.address, - serverPort: site.listenPort + serverPort: site.listenPort, + remoteSubnets: site.remoteSubnets }); } diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index ce887b98..2d6ed98b 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -2,9 +2,16 @@ import { z } from "zod"; import { MessageHandler } from "../ws"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { db, ExitNode, exitNodes } from "@server/db"; +import { + db, + ExitNode, + exitNodes, + resources, + Target, + targets +} from "@server/db"; import { clients, clientSites, Newt, sites } from "@server/db"; -import { eq } from "drizzle-orm"; +import { eq, and, inArray } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; import axios from "axios"; @@ -191,7 +198,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { endpoint: endpoint, publicKey: site.publicKey, serverIP: site.address, - serverPort: site.listenPort + serverPort: site.listenPort, + remoteSubnets: site.remoteSubnets }); } catch (error) { logger.error( @@ -212,14 +220,96 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { // Filter out any null values from peers that didn't have an olm const validPeers = peers.filter((peer) => peer !== null); + // Improved version + const allResources = await db.transaction(async (tx) => { + // First get all resources for the site + const resourcesList = await tx + .select({ + resourceId: resources.resourceId, + subdomain: resources.subdomain, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + blockAccess: resources.blockAccess, + sso: resources.sso, + emailWhitelistEnabled: resources.emailWhitelistEnabled, + http: resources.http, + proxyPort: resources.proxyPort, + protocol: resources.protocol + }) + .from(resources) + .where(and(eq(resources.siteId, siteId), eq(resources.http, false))); + + // Get all enabled targets for these resources in a single query + const resourceIds = resourcesList.map((r) => r.resourceId); + const allTargets = + resourceIds.length > 0 + ? await tx + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled, + }) + .from(targets) + .where( + and( + inArray(targets.resourceId, resourceIds), + eq(targets.enabled, true) + ) + ) + : []; + + // Combine the data in JS instead of using SQL for the JSON + return resourcesList.map((resource) => ({ + ...resource, + targets: allTargets.filter( + (target) => target.resourceId === resource.resourceId + ) + })); + }); + + const { tcpTargets, udpTargets } = allResources.reduce( + (acc, resource) => { + // Skip resources with no targets + if (!resource.targets?.length) return acc; + + // Format valid targets into strings + const formattedTargets = resource.targets + .filter( + (target: Target) => + resource.proxyPort && target?.ip && target?.port + ) + .map( + (target: Target) => + `${resource.proxyPort}:${target.ip}:${target.port}` + ); + + // Add to the appropriate protocol array + if (resource.protocol === "tcp") { + acc.tcpTargets.push(...formattedTargets); + } else { + acc.udpTargets.push(...formattedTargets); + } + + return acc; + }, + { tcpTargets: [] as string[], udpTargets: [] as string[] } + ); + // Build the configuration response const configResponse = { ipAddress: site.address, - peers: validPeers + peers: validPeers, + targets: { + udp: udpTargets, + tcp: tcpTargets + } }; logger.debug("Sending config: ", configResponse); - return { message: { type: "newt/wg/receive-config", diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index d3c541a6..642fc2df 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -4,7 +4,8 @@ import { sendToClient } from "../ws"; export function addTargets( newtId: string, targets: Target[], - protocol: string + protocol: string, + port: number | null = null ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -13,19 +14,32 @@ export function addTargets( }:${target.port}`; }); - const payload = { + sendToClient(newtId, { type: `newt/${protocol}/add`, data: { targets: payloadTargets } - }; - sendToClient(newtId, payload); + }); + + const payloadTargetsResources = targets.map((target) => { + return `${port ? port + ":" : ""}${ + target.ip + }:${target.port}`; + }); + + sendToClient(newtId, { + type: `newt/wg/${protocol}/add`, + data: { + targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now + } + }); } export function removeTargets( newtId: string, targets: Target[], - protocol: string + protocol: string, + port: number | null = null ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -34,11 +48,23 @@ export function removeTargets( }:${target.port}`; }); - const payload = { + sendToClient(newtId, { type: `newt/${protocol}/remove`, data: { targets: payloadTargets } - }; - sendToClient(newtId, payload); + }); + + const payloadTargetsResources = targets.map((target) => { + return `${port ? port + ":" : ""}${ + target.ip + }:${target.port}`; + }); + + sendToClient(newtId, { + type: `newt/wg/${protocol}/remove`, + data: { + targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now + } + }); } diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index f504ecd7..8a73daff 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -119,12 +119,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { continue; } - if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { - logger.warn( - `Site ${site.siteId} last hole punch is too old, skipping` - ); - continue; - } + // if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { + // logger.warn( + // `Site ${site.siteId} last hole punch is too old, skipping` + // ); + // continue; + // } // If public key changed, delete old peer from this site if (client.pubKey && client.pubKey != publicKey) { @@ -175,7 +175,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { endpoint: endpoint, publicKey: site.publicKey, serverIP: site.address, - serverPort: site.listenPort + serverPort: site.listenPort, + remoteSubnets: site.remoteSubnets }); } diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 48a915aa..c47c84a8 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -12,6 +12,7 @@ export async function addPeer( endpoint: string; serverIP: string | null; serverPort: number | null; + remoteSubnets: string | null; // optional, comma-separated list of subnets that this site can access } ) { const [olm] = await db @@ -30,7 +31,8 @@ export async function addPeer( publicKey: peer.publicKey, endpoint: peer.endpoint, serverIP: peer.serverIP, - serverPort: peer.serverPort + serverPort: peer.serverPort, + remoteSubnets: peer.remoteSubnets // optional, comma-separated list of subnets that this site can access } }); @@ -66,6 +68,7 @@ export async function updatePeer( endpoint: string; serverIP: string | null; serverPort: number | null; + remoteSubnets?: string | null; // optional, comma-separated list of subnets that } ) { const [olm] = await db @@ -84,7 +87,8 @@ export async function updatePeer( publicKey: peer.publicKey, endpoint: peer.endpoint, serverIP: peer.serverIP, - serverPort: peer.serverPort + serverPort: peer.serverPort, + remoteSubnets: peer.remoteSubnets } }); diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 8f16b198..dfbb7617 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -40,7 +40,7 @@ const createHttpResourceSchema = z siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - domainId: z.string() + domainId: z.string(), }) .strict() .refine( @@ -59,7 +59,8 @@ const createRawResourceSchema = z siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().min(1).max(65535) + proxyPort: z.number().int().min(1).max(65535), + enableProxy: z.boolean().default(true) }) .strict() .refine( @@ -378,7 +379,7 @@ async function createRawResource( ); } - const { name, http, protocol, proxyPort } = parsedBody.data; + const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data; // if http is false check to see if there is already a resource with the same port and protocol const existingResource = await db @@ -411,7 +412,8 @@ async function createRawResource( name, http, protocol, - proxyPort + proxyPort, + enableProxy }) .returning(); diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index bb9a6f32..99adc5f7 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -103,7 +103,8 @@ export async function deleteResource( removeTargets( newt.newtId, targetsToBeRemoved, - deletedResource.protocol + deletedResource.protocol, + deletedResource.proxyPort ); } } diff --git a/server/routers/resource/transferResource.ts b/server/routers/resource/transferResource.ts index e0fce278..a99405df 100644 --- a/server/routers/resource/transferResource.ts +++ b/server/routers/resource/transferResource.ts @@ -168,7 +168,8 @@ export async function transferResource( removeTargets( newt.newtId, resourceTargets, - updatedResource.protocol + updatedResource.protocol, + updatedResource.proxyPort ); } } @@ -190,7 +191,8 @@ export async function transferResource( addTargets( newt.newtId, resourceTargets, - updatedResource.protocol + updatedResource.protocol, + updatedResource.proxyPort ); } } diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index a20a7024..e99c6e8b 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -93,7 +93,8 @@ const updateRawResourceBodySchema = z name: z.string().min(1).max(255).optional(), proxyPort: z.number().int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), - enabled: z.boolean().optional() + enabled: z.boolean().optional(), + enableProxy: z.boolean().optional(), }) .strict() .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 52bd0417..ffea1571 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -173,7 +173,7 @@ export async function createTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, newTarget, resource.protocol); + addTargets(newt.newtId, newTarget, resource.protocol, resource.proxyPort); } } } diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 17a9c5ee..6eadeccd 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -105,7 +105,7 @@ export async function deleteTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - removeTargets(newt.newtId, [deletedTarget], resource.protocol); + removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort); } } diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 0138520b..0b7c4692 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -157,7 +157,7 @@ export async function updateTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, [updatedTarget], resource.protocol); + addTargets(newt.newtId, [updatedTarget], resource.protocol, resource.proxyPort); } } return response(res, { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index c876de22..882a296a 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -66,7 +66,8 @@ export async function traefikConfigProvider( enabled: resources.enabled, stickySession: resources.stickySession, tlsServerName: resources.tlsServerName, - setHostHeader: resources.setHostHeader + setHostHeader: resources.setHostHeader, + enableProxy: resources.enableProxy }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -365,6 +366,10 @@ export async function traefikConfigProvider( } } else { // Non-HTTP (TCP/UDP) configuration + if (!resource.enableProxy) { + continue; + } + const protocol = resource.protocol.toLowerCase(); const port = resource.proxyPort; diff --git a/server/setup/scriptsSqlite/1.8.0.ts b/server/setup/scriptsSqlite/1.8.0.ts new file mode 100644 index 00000000..efb4f68a --- /dev/null +++ b/server/setup/scriptsSqlite/1.8.0.ts @@ -0,0 +1,29 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.8.0"; + +export default async function migration() { + console.log("Running setup script ${version}..."); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + db.transaction(() => { + db.exec(` + ALTER TABLE 'user' ADD 'termsAcceptedTimestamp' text; + ALTER TABLE 'user' ADD 'termsVersion' text; + ALTER TABLE 'sites' ADD 'remoteSubnets' text; + `); + })(); + + console.log("Migrated database schema"); + } catch (e) { + console.log("Unable to migrate database schema"); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx index b798bf93..83cc11e3 100644 --- a/src/app/[orgId]/settings/clients/page.tsx +++ b/src/app/[orgId]/settings/clients/page.tsx @@ -48,7 +48,7 @@ export default async function ClientsPage(props: ClientsPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx index 717e4d49..cc4408b2 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx @@ -18,6 +18,7 @@ import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { RotateCw } from "lucide-react"; import { createApiClient } from "@app/lib/api"; +import { build } from "@server/build"; type ResourceInfoBoxType = {}; @@ -34,7 +35,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { - {t('resourceInfo')} + {t("resourceInfo")} @@ -42,7 +43,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { <> - {t('authentication')} + {t("authentication")} {authInfo.password || @@ -51,12 +52,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { authInfo.whitelist ? (
- {t('protected')} + {t("protected")}
) : (
- {t('notProtected')} + {t("notProtected")}
)}
@@ -71,7 +72,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
- {t('site')} + {t("site")} {resource.siteName} @@ -98,7 +99,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { ) : ( <> - {t('protocol')} + + {t("protocol")} + {resource.protocol.toUpperCase()} @@ -106,7 +109,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { - {t('port')} + {t("port")} + {build == "oss" && ( + + + {t("externalProxyEnabled")} + + + + {resource.enableProxy + ? t("enabled") + : t("disabled")} + + + + )} )} - {t('visibility')} + {t("visibility")} - {resource.enabled ? t('enabled') : t('disabled')} + {resource.enabled + ? t("enabled") + : t("disabled")} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index efda61c3..266911a6 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -66,6 +66,7 @@ import { } from "@server/routers/resource"; import { SwitchInput } from "@app/components/SwitchInput"; import { useTranslations } from "next-intl"; +import { Checkbox } from "@app/components/ui/checkbox"; import { Credenza, CredenzaBody, @@ -78,6 +79,7 @@ import { } from "@app/components/Credenza"; import DomainPicker from "@app/components/DomainPicker"; import { Globe } from "lucide-react"; +import { build } from "@server/build"; const TransferFormSchema = z.object({ siteId: z.number() @@ -118,25 +120,31 @@ export default function GeneralForm() { fullDomain: string; } | null>(null); - const GeneralFormSchema = z.object({ - enabled: z.boolean(), - subdomain: z.string().optional(), - name: z.string().min(1).max(255), - domainId: z.string().optional(), - proxyPort: z.number().int().min(1).max(65535).optional() - }).refine((data) => { - // For non-HTTP resources, proxyPort should be defined - if (!resource.http) { - return data.proxyPort !== undefined; - } - // For HTTP resources, proxyPort should be undefined - return data.proxyPort === undefined; - }, { - message: !resource.http - ? "Port number is required for non-HTTP resources" - : "Port number should not be set for HTTP resources", - path: ["proxyPort"] - }); + const GeneralFormSchema = z + .object({ + enabled: z.boolean(), + subdomain: z.string().optional(), + name: z.string().min(1).max(255), + domainId: z.string().optional(), + proxyPort: z.number().int().min(1).max(65535).optional(), + enableProxy: z.boolean().optional() + }) + .refine( + (data) => { + // For non-HTTP resources, proxyPort should be defined + if (!resource.http) { + return data.proxyPort !== undefined; + } + // For HTTP resources, proxyPort should be undefined + return data.proxyPort === undefined; + }, + { + message: !resource.http + ? "Port number is required for non-HTTP resources" + : "Port number should not be set for HTTP resources", + path: ["proxyPort"] + } + ); type GeneralFormValues = z.infer; @@ -147,7 +155,8 @@ export default function GeneralForm() { name: resource.name, subdomain: resource.subdomain ? resource.subdomain : undefined, domainId: resource.domainId || undefined, - proxyPort: resource.proxyPort || undefined + proxyPort: resource.proxyPort || undefined, + enableProxy: resource.enableProxy || false }, mode: "onChange" }); @@ -211,7 +220,8 @@ export default function GeneralForm() { name: data.name, subdomain: data.subdomain, domainId: data.domainId, - proxyPort: data.proxyPort + proxyPort: data.proxyPort, + enableProxy: data.enableProxy } ) .catch((e) => { @@ -238,7 +248,8 @@ export default function GeneralForm() { name: data.name, subdomain: data.subdomain, fullDomain: resource.fullDomain, - proxyPort: data.proxyPort + proxyPort: data.proxyPort, + enableProxy: data.enableProxy }); router.refresh(); @@ -357,16 +368,29 @@ export default function GeneralForm() { render={({ field }) => ( - {t("resourcePortNumber")} + {t( + "resourcePortNumber" + )} + value={ + field.value ?? + "" + } + onChange={( + e + ) => field.onChange( - e.target.value - ? parseInt(e.target.value) + e + .target + .value + ? parseInt( + e + .target + .value + ) : undefined ) } @@ -374,11 +398,49 @@ export default function GeneralForm() { - {t("resourcePortNumberDescription")} + {t( + "resourcePortNumberDescription" + )} )} /> + + {build == "oss" && ( + ( + + + + +
+ + {t( + "resourceEnableProxy" + )} + + + {t( + "resourceEnableProxyDescription" + )} + +
+
+ )} + /> + )} )} diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index 22e9d90c..a916a700 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -25,6 +25,7 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Input } from "@app/components/ui/input"; import { Button } from "@app/components/ui/button"; +import { Checkbox } from "@app/components/ui/checkbox"; import { useParams, useRouter } from "next/navigation"; import { ListSitesResponse } from "@server/routers/site"; import { formatAxiosError } from "@app/lib/api"; @@ -64,6 +65,7 @@ import CopyTextBox from "@app/components/CopyTextBox"; import Link from "next/link"; import { useTranslations } from "next-intl"; import DomainPicker from "@app/components/DomainPicker"; +import { build } from "@server/build"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), @@ -78,7 +80,8 @@ const httpResourceFormSchema = z.object({ const tcpUdpResourceFormSchema = z.object({ protocol: z.string(), - proxyPort: z.number().int().min(1).max(65535) + proxyPort: z.number().int().min(1).max(65535), + enableProxy: z.boolean().default(false) }); type BaseResourceFormValues = z.infer; @@ -144,7 +147,8 @@ export default function Page() { resolver: zodResolver(tcpUdpResourceFormSchema), defaultValues: { protocol: "tcp", - proxyPort: undefined + proxyPort: undefined, + enableProxy: false } }); @@ -163,16 +167,17 @@ export default function Page() { if (isHttp) { const httpData = httpForm.getValues(); - Object.assign(payload, { - subdomain: httpData.subdomain, - domainId: httpData.domainId, - protocol: "tcp", - }); + Object.assign(payload, { + subdomain: httpData.subdomain, + domainId: httpData.domainId, + protocol: "tcp" + }); } else { const tcpUdpData = tcpUdpForm.getValues(); Object.assign(payload, { protocol: tcpUdpData.protocol, - proxyPort: tcpUdpData.proxyPort + proxyPort: tcpUdpData.proxyPort, + enableProxy: tcpUdpData.enableProxy }); } @@ -198,8 +203,15 @@ export default function Page() { if (isHttp) { router.push(`/${orgId}/settings/resources/${id}`); } else { - setShowSnippets(true); - router.refresh(); + const tcpUdpData = tcpUdpForm.getValues(); + // Only show config snippets if enableProxy is explicitly true + if (tcpUdpData.enableProxy === true) { + setShowSnippets(true); + router.refresh(); + } else { + // If enableProxy is false or undefined, go directly to resource page + router.push(`/${orgId}/settings/resources/${id}`); + } } } } catch (e) { @@ -603,6 +615,46 @@ export default function Page() { )} /> + + {build == "oss" && ( + ( + + + + +
+ + {t( + "resourceEnableProxy" + )} + + + {t( + "resourceEnableProxyDescription" + )} + +
+
+ )} + /> + )} From e105a523e41d071ddabcb21a6923ef74ac962bb4 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 27 Jul 2025 14:11:36 -0700 Subject: [PATCH 26/64] Add log and fix default --- install/config/config.yml | 2 +- server/routers/gerbil/updateHolePunch.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/install/config/config.yml b/install/config/config.yml index 5f81c141..fc41cfe0 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -24,7 +24,7 @@ gerbil: orgs: block_size: 24 - subnet_group: 100.89.138.0/20 + subnet_group: 100.90.128.0/24 {{if .EnableEmail}} email: diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index e99225fe..4910738e 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -125,6 +125,8 @@ export async function updateHolePunch( } } else if (newtId) { + logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`); + const { session, newt: newtSession } = await validateNewtSessionToken(token); From 2ca8febff7b07e4f192381ab14ef20800e687f18 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 27 Jul 2025 14:12:01 -0700 Subject: [PATCH 27/64] We dont need this config --- install/config/config.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/install/config/config.yml b/install/config/config.yml index fc41cfe0..00d7c897 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -22,10 +22,6 @@ gerbil: start_port: 51820 base_endpoint: "{{.DashboardDomain}}" -orgs: - block_size: 24 - subnet_group: 100.90.128.0/24 - {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" From 67bae760489c58cf4b55c0eff061c5a1488ebb2a Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Jul 2025 12:21:15 -0700 Subject: [PATCH 28/64] minor visual tweaks to member landing --- src/app/[orgId]/MemberResourcesPortal.tsx | 538 +++++++++++----------- src/app/[orgId]/page.tsx | 6 +- src/components/ui/info-popup.tsx | 12 +- 3 files changed, 275 insertions(+), 281 deletions(-) diff --git a/src/app/[orgId]/MemberResourcesPortal.tsx b/src/app/[orgId]/MemberResourcesPortal.tsx index 142d5516..ad412b1e 100644 --- a/src/app/[orgId]/MemberResourcesPortal.tsx +++ b/src/app/[orgId]/MemberResourcesPortal.tsx @@ -4,21 +4,42 @@ import { useState, useEffect } from "react"; import { useTranslations } from "next-intl"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ExternalLink, Globe, ShieldCheck, Search, RefreshCw, AlertCircle, Plus, Shield, ShieldOff, ChevronLeft, ChevronRight, Building2, Key, KeyRound, Fingerprint, AtSign, Copy, InfoIcon } from "lucide-react"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + ExternalLink, + Globe, + Search, + RefreshCw, + AlertCircle, + ChevronLeft, + ChevronRight, + Key, + KeyRound, + Fingerprint, + AtSign, + Copy, + InfoIcon, + Combine +} from "lucide-react"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { GetUserResourcesResponse } from "@server/routers/resource/getUserResources"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { useToast } from "@app/hooks/useToast"; +import { InfoPopup } from "@/components/ui/info-popup"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "@/components/ui/tooltip"; // Update Resource type to include site information type Resource = { @@ -42,26 +63,34 @@ type MemberResourcesPortalProps = { }; // Favicon component with fallback -const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean }) => { +const ResourceFavicon = ({ + domain, + enabled +}: { + domain: string; + enabled: boolean; +}) => { const [faviconError, setFaviconError] = useState(false); const [faviconLoaded, setFaviconLoaded] = useState(false); - + // Extract domain for favicon URL - const cleanDomain = domain.replace(/^https?:\/\//, '').split('/')[0]; + const cleanDomain = domain.replace(/^https?:\/\//, "").split("/")[0]; const faviconUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=32`; - + const handleFaviconLoad = () => { setFaviconLoaded(true); setFaviconError(false); }; - + const handleFaviconError = () => { setFaviconError(true); setFaviconLoaded(false); }; if (faviconError || !enabled) { - return ; + return ( + + ); } return ( @@ -72,7 +101,7 @@ const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean {`${cleanDomain} @@ -80,198 +109,107 @@ const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean ); }; -// Enhanced status badge component -const StatusBadge = ({ enabled, protected: isProtected, resource }: { enabled: boolean; protected: boolean; resource: Resource }) => { - if (!enabled) { - return ( - - - -
-
-
-
- -

Resource Disabled

-
-
-
- ); - } - - if (isProtected) { - return ( - - - -
- -
-
- -

Protected Resource

-
-

Authentication Methods:

-
- {resource.sso && ( -
-
- -
- Single Sign-On (SSO) -
- )} - {resource.password && ( -
-
- -
- Password Protected -
- )} - {resource.pincode && ( -
-
- -
- PIN Code -
- )} - {resource.whitelist && ( -
-
- -
- Email Whitelist -
- )} -
-
-
-
-
- ); - } - - return ( -
- -
- ); -}; - // Resource Info component const ResourceInfo = ({ resource }: { resource: Resource }) => { - const hasAuthMethods = resource.sso || resource.password || resource.pincode || resource.whitelist; - - return ( - - - -
- + const hasAuthMethods = + resource.sso || + resource.password || + resource.pincode || + resource.whitelist; + + const infoContent = ( +
+ {/* Site Information */} + {resource.siteName && ( +
+
Site
+
+ + {resource.siteName}
- - - {/* Site Information */} - {resource.siteName && ( -
-
Site
-
- - {resource.siteName} -
-
- )} +
+ )} - {/* Authentication Methods */} - {hasAuthMethods && ( -
-
Authentication Methods
-
- {resource.sso && ( -
-
- -
- Single Sign-On (SSO) -
- )} - {resource.password && ( -
-
- -
- Password Protected -
- )} - {resource.pincode && ( -
-
- -
- PIN Code -
- )} - {resource.whitelist && ( -
-
- -
- Email Whitelist -
- )} -
-
- )} - - {/* Resource Status - if disabled */} - {!resource.enabled && ( -
-
- - Resource Disabled -
-
- )} - - - - ); -}; - -// Site badge component -const SiteBadge = ({ resource }: { resource: Resource }) => { - if (!resource.siteName) { - return null; - } - - return ( - - - -
- + {/* Authentication Methods */} + {hasAuthMethods && ( +
+
+ Authentication Methods
- - -

{resource.siteName}

-
- - +
+ {resource.sso && ( +
+
+ +
+ + Single Sign-On (SSO) + +
+ )} + {resource.password && ( +
+
+ +
+ + Password Protected + +
+ )} + {resource.pincode && ( +
+
+ +
+ PIN Code +
+ )} + {resource.whitelist && ( +
+
+ +
+ Email Whitelist +
+ )} +
+
+ )} + + {/* Resource Status - if disabled */} + {!resource.enabled && ( +
+
+ + + Resource Disabled + +
+
+ )} +
); + + return {infoContent}; }; // Pagination component -const PaginationControls = ({ - currentPage, - totalPages, +const PaginationControls = ({ + currentPage, + totalPages, onPageChange, totalItems, - itemsPerPage -}: { - currentPage: number; - totalPages: number; + itemsPerPage +}: { + currentPage: number; + totalPages: number; onPageChange: (page: number) => void; totalItems: number; itemsPerPage: number; @@ -286,7 +224,7 @@ const PaginationControls = ({
Showing {startItem}-{endItem} of {totalItems} resources
- +
- +
- {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => { - // Show first page, last page, current page, and 2 pages around current - const showPage = - page === 1 || - page === totalPages || - Math.abs(page - currentPage) <= 1; - - const showEllipsis = - (page === 2 && currentPage > 4) || - (page === totalPages - 1 && currentPage < totalPages - 3); + {Array.from({ length: totalPages }, (_, i) => i + 1).map( + (page) => { + // Show first page, last page, current page, and 2 pages around current + const showPage = + page === 1 || + page === totalPages || + Math.abs(page - currentPage) <= 1; - if (!showPage && !showEllipsis) return null; + const showEllipsis = + (page === 2 && currentPage > 4) || + (page === totalPages - 1 && + currentPage < totalPages - 3); + + if (!showPage && !showEllipsis) return null; + + if (showEllipsis) { + return ( + + ... + + ); + } - if (showEllipsis) { return ( - - ... - + ); } - - return ( - - ); - })} + )}
- +
- + {/* Sort */}
@@ -595,7 +567,9 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr disabled={refreshing} className="gap-2 shrink-0" > - + Refresh
@@ -603,7 +577,7 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr {/* Resources Content */} {filteredResources.length === 0 ? ( /* Enhanced Empty State */ - +
{searchQuery ? ( @@ -613,17 +587,18 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr )}

- {searchQuery ? "No Resources Found" : "No Resources Available"} + {searchQuery + ? "No Resources Found" + : "No Resources Available"}

- {searchQuery + {searchQuery ? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.` - : "You don't have access to any resources yet. Contact your administrator to get access to resources you need." - } + : "You don't have access to any resources yet. Contact your administrator to get access to resources you need."}

{searchQuery ? ( - ) : ( - )} @@ -649,12 +626,15 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr {/* Resources Grid */}
{paginatedResources.map((resource) => ( - +
- +
@@ -664,12 +644,14 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr -

{resource.name}

+

+ {resource.name} +

- +
@@ -677,21 +659,29 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
-
); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 9a1dda94..4740198b 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -62,11 +62,7 @@ export default async function OrgPage(props: OrgPageProps) { return ( - {overview && ( -
- -
- )} + {overview && }
); diff --git a/src/components/ui/info-popup.tsx b/src/components/ui/info-popup.tsx index 732c23e9..cff1cce4 100644 --- a/src/components/ui/info-popup.tsx +++ b/src/components/ui/info-popup.tsx @@ -11,11 +11,12 @@ import { Button } from "@/components/ui/button"; interface InfoPopupProps { text?: string; - info: string; + info?: string; trigger?: React.ReactNode; + children?: React.ReactNode; } -export function InfoPopup({ text, info, trigger }: InfoPopupProps) { +export function InfoPopup({ text, info, trigger, children }: InfoPopupProps) { const defaultTrigger = (
From 80aa7502af1abeffdd52d1ec0691b3792713d5d2 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Jul 2025 12:52:44 -0700 Subject: [PATCH 29/64] fix resource domain not required --- messages/en-US.json | 4 +-- .../resources/[resourceId]/general/page.tsx | 1 + .../settings/resources/create/page.tsx | 10 +++--- src/app/navigation.tsx | 2 +- src/components/DomainPicker.tsx | 33 ++++++++++--------- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 4ff1b866..e69e2b46 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "All Users", "sidebarIdentityProviders": "Identity Providers", "sidebarLicense": "License", - "sidebarClients": "Clients (beta)", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", "enableDockerSocket": "Enable Docker Socket", "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", @@ -1319,4 +1319,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 266911a6..68cc02cc 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -636,6 +636,7 @@ export default function GeneralForm() { { const selected = { domainId: res.domainId, diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index a916a700..fc90d26c 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -74,7 +74,7 @@ const baseResourceFormSchema = z.object({ }); const httpResourceFormSchema = z.object({ - domainId: z.string().optional(), + domainId: z.string().nonempty(), subdomain: z.string().optional() }); @@ -277,9 +277,9 @@ export default function Page() { if (res?.status === 200) { const domains = res.data.data.domains; setBaseDomains(domains); - if (domains.length) { - httpForm.setValue("domainId", domains[0].domainId); - } + // if (domains.length) { + // httpForm.setValue("domainId", domains[0].domainId); + // } } }; @@ -684,6 +684,8 @@ export default function Page() { ? await httpForm.trigger() : await tcpUdpForm.trigger(); + console.log(httpForm.getValues()); + if (baseValid && settingsValid) { onSubmit(); } diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 9901ee2f..b26b98ec 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -134,4 +134,4 @@ export const adminNavSections: SidebarNavSection[] = [ : []) ] } -]; \ No newline at end of file +]; diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 1b96ec8e..28dbcdbd 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -49,6 +49,7 @@ type DomainOption = { interface DomainPickerProps { orgId: string; + cols?: number; onDomainChange?: (domainInfo: { domainId: string; domainNamespaceId?: string; @@ -61,6 +62,7 @@ interface DomainPickerProps { export default function DomainPicker({ orgId, + cols, onDomainChange }: DomainPickerProps) { const { env } = useEnvContext(); @@ -309,6 +311,7 @@ export default function DomainPicker({ { // Only allow letters, numbers, hyphens, and periods const validInput = e.target.value.replace( @@ -393,23 +396,25 @@ export default function DomainPicker({ {/* Organization Domains */} {organizationOptions.length > 0 && (
-
- -

- {t("domainPickerOrganizationDomains")} -

-
-
+ {build !== "oss" && ( +
+ +

+ {t("domainPickerOrganizationDomains")} +

+
+ )} +
{organizationOptions.map((option) => (
@@ -456,10 +461,6 @@ export default function DomainPicker({

)}
- {selectedOption?.id === - option.id && ( - - )}
))} @@ -476,14 +477,14 @@ export default function DomainPicker({ {t("domainPickerProvidedDomains")}
-
+
{providedOptions.map((option) => (
Date: Mon, 28 Jul 2025 12:53:00 -0700 Subject: [PATCH 30/64] Dont send enableProxy --- server/routers/resource/createResource.ts | 21 +++++++------------ server/routers/resource/updateResource.ts | 21 ++++++++----------- .../resources/[resourceId]/general/page.tsx | 8 +++++-- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index dfbb7617..8c80c90c 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -33,14 +33,11 @@ const createResourceParamsSchema = z const createHttpResourceSchema = z .object({ name: z.string().min(1).max(255), - subdomain: z - .string() - .nullable() - .optional(), + subdomain: z.string().nullable().optional(), siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - domainId: z.string(), + domainId: z.string() }) .strict() .refine( @@ -51,7 +48,7 @@ const createHttpResourceSchema = z return true; }, { message: "Invalid subdomain" } - ) + ); const createRawResourceSchema = z .object({ @@ -89,12 +86,7 @@ registry.registerPath({ body: { content: { "application/json": { - schema: - build == "oss" - ? createHttpResourceSchema.or( - createRawResourceSchema - ) - : createHttpResourceSchema + schema: createHttpResourceSchema.or(createRawResourceSchema) } } } @@ -157,7 +149,10 @@ export async function createResource( { siteId, orgId } ); } else { - if (!config.getRawConfig().flags?.allow_raw_resources && build == "oss") { + if ( + !config.getRawConfig().flags?.allow_raw_resources && + build == "oss" + ) { return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index e99c6e8b..5cf68c2b 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -34,9 +34,7 @@ const updateResourceParamsSchema = z const updateHttpResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), - subdomain: subdomainSchema - .nullable() - .optional(), + subdomain: subdomainSchema.nullable().optional(), ssl: z.boolean().optional(), sso: z.boolean().optional(), blockAccess: z.boolean().optional(), @@ -94,7 +92,7 @@ const updateRawResourceBodySchema = z proxyPort: z.number().int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), enabled: z.boolean().optional(), - enableProxy: z.boolean().optional(), + enableProxy: z.boolean().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -122,12 +120,9 @@ registry.registerPath({ body: { content: { "application/json": { - schema: - build == "oss" - ? updateHttpResourceBodySchema.and( - updateRawResourceBodySchema - ) - : updateHttpResourceBodySchema + schema: updateHttpResourceBodySchema.and( + updateRawResourceBodySchema + ) } } } @@ -289,7 +284,9 @@ async function updateHttpResource( } else if (domainRes.domains.type == "wildcard") { if (updateData.subdomain !== undefined) { // the subdomain cant have a dot in it - const parsedSubdomain = subdomainSchema.safeParse(updateData.subdomain); + const parsedSubdomain = subdomainSchema.safeParse( + updateData.subdomain + ); if (!parsedSubdomain.success) { return next( createHttpError( @@ -342,7 +339,7 @@ async function updateHttpResource( const updatedResource = await db .update(resources) - .set({...updateData, }) + .set({ ...updateData }) .where(eq(resources.resourceId, resource.resourceId)) .returning(); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 68cc02cc..b4e14d64 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -221,7 +221,9 @@ export default function GeneralForm() { subdomain: data.subdomain, domainId: data.domainId, proxyPort: data.proxyPort, - enableProxy: data.enableProxy + ...(!resource.http && { + enableProxy: data.enableProxy + }) } ) .catch((e) => { @@ -249,7 +251,9 @@ export default function GeneralForm() { subdomain: data.subdomain, fullDomain: resource.fullDomain, proxyPort: data.proxyPort, - enableProxy: data.enableProxy + ...(!resource.http && { + enableProxy: data.enableProxy + }), }); router.refresh(); From 494329f568ddea3ea08352031add0fa58e83e0dd Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Jul 2025 12:55:11 -0700 Subject: [PATCH 31/64] delete resources on delete org --- server/routers/org/deleteOrg.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index 41b491a2..2f4ddf9e 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, resources } from "@server/db"; import { newts, newtSessions, @@ -127,6 +127,7 @@ export async function deleteOrg( } await trx.delete(orgs).where(eq(orgs.orgId, orgId)); + await trx.delete(resources).where(eq(resources.orgId, orgId)); }); // Send termination messages outside of transaction to prevent blocking From 3fc72dbec2ed7e1534191a07954344544b1ee210 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:11 -0700 Subject: [PATCH 32/64] New translations en-us.json (French) --- messages/fr-FR.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 235c6a7c..f41b3881 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Erreur lors de la création du site", "siteErrorCreateKeyPair": "Paire de clés ou site par défaut introuvable", "siteErrorCreateDefaults": "Les valeurs par défaut du site sont introuvables", - "siteNameDescription": "Ceci est le nom d'affichage du site.", "method": "Méthode", "siteMethodDescription": "C'est ainsi que vous exposerez les connexions.", "siteLearnNewt": "Apprenez à installer Newt sur votre système", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Tous les utilisateurs", "sidebarIdentityProviders": "Fournisseurs d'identité", "sidebarLicense": "Licence", - "sidebarClients": "Clients", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domaines", "enableDockerSocket": "Activer Docker Socket", "enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du socket doit être fourni à Newt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Domaine unique (CNAME)", "selectDomainTypeCnameDescription": "Juste ce domaine spécifique. Utilisez ce paramètre pour des sous-domaines individuels ou des entrées de domaine spécifiques.", "selectDomainTypeWildcardName": "Domaine Générique", - "selectDomainTypeWildcardDescription": "Ce domaine et son premier niveau de sous-domaines.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Domaine Unique", "selectType": "Sélectionnez un type", "actions": "Actions", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Développer", "newtUpdateAvailable": "Mise à jour disponible", "newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.", - "domainPickerEnterDomain": "Entrez votre domaine", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, ou simplement myapp", "domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.", "domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Vérification de la disponibilité...", - "domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé pour \"{userInput}\". Essayez un autre domaine ou vérifiez les paramètres de domaine de votre organisation.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Domaines de l'organisation", "domainPickerProvidedDomains": "Domaines fournis", "domainPickerSubdomain": "Sous-domaine : {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Nom :", "createDomainValue": "Valeur :", "createDomainCnameRecords": "Enregistrements CNAME", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Enregistrement {number}", "createDomainTxtRecords": "Enregistrements TXT", "createDomainSaveTheseRecords": "Enregistrez ces enregistrements", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "Propagation DNS", "createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.", "resourcePortRequired": "Le numéro de port est requis pour les ressources non-HTTP", - "resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP" + "resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 1dcac85c0d126de28f21035ec48fd46b39deb42a Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:12 -0700 Subject: [PATCH 33/64] New translations en-us.json (Spanish) --- messages/es-ES.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index f7c208bb..62d5ab80 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Error al crear el sitio", "siteErrorCreateKeyPair": "Por defecto no se encuentra el par de claves o el sitio", "siteErrorCreateDefaults": "Sitio por defecto no encontrado", - "siteNameDescription": "Este es el nombre para mostrar el sitio.", "method": "Método", "siteMethodDescription": "Así es como se expondrán las conexiones.", "siteLearnNewt": "Aprende cómo instalar Newt en tu sistema", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Todos los usuarios", "sidebarIdentityProviders": "Proveedores de identidad", "sidebarLicense": "Licencia", - "sidebarClients": "Clientes", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Dominios", "enableDockerSocket": "Habilitar conector Docker", "enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. La ruta del socket debe proporcionarse a Newt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Dominio único (CNAME)", "selectDomainTypeCnameDescription": "Solo este dominio específico. Úsalo para subdominios individuales o entradas específicas de dominio.", "selectDomainTypeWildcardName": "Dominio comodín", - "selectDomainTypeWildcardDescription": "Este dominio y su primer nivel de subdominios.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Dominio único", "selectType": "Selecciona un tipo", "actions": "Acciones", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Expandir", "newtUpdateAvailable": "Nueva actualización disponible", "newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.", - "domainPickerEnterDomain": "Ingresa tu dominio", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.miDominio.com, o solo myapp", "domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.", "domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Comprobando disponibilidad...", - "domainPickerNoMatchingDomains": "No se encontraron dominios coincidentes para \"{userInput}\". Prueba con un dominio diferente o revisa la configuración de dominio de tu organización.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Dominios de la organización", "domainPickerProvidedDomains": "Dominios proporcionados", "domainPickerSubdomain": "Subdominio: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Nombre:", "createDomainValue": "Valor:", "createDomainCnameRecords": "Registros CNAME", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Registro {number}", "createDomainTxtRecords": "Registros TXT", "createDomainSaveTheseRecords": "Guardar estos registros", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "Propagación DNS", "createDomainDnsPropagationDescription": "Los cambios de DNS pueden tardar un tiempo en propagarse a través de internet. Esto puede tardar desde unos pocos minutos hasta 48 horas, dependiendo de tu proveedor de DNS y la configuración de TTL.", "resourcePortRequired": "Se requiere número de puerto para recursos no HTTP", - "resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP" + "resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 0c1e20ba482356545d25028aefe9875950e7cb37 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:13 -0700 Subject: [PATCH 34/64] New translations en-us.json (Czech) --- messages/cs-CZ.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 1416cede..043367f9 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Chyba při vytváření lokality", "siteErrorCreateKeyPair": "Nebyly nalezeny klíče nebo výchozí nastavení lokality", "siteErrorCreateDefaults": "Výchozí nastavení lokality nenalezeno", - "siteNameDescription": "Toto je zobrazovaný název lokality.", "method": "Způsob", "siteMethodDescription": "Tímto způsobem budete vystavovat spojení.", "siteLearnNewt": "Naučte se, jak nainstalovat Newt na svůj systém", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "All Users", "sidebarIdentityProviders": "Identity Providers", "sidebarLicense": "License", - "sidebarClients": "Clients", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", "enableDockerSocket": "Enable Docker Socket", "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Single Domain (CNAME)", "selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.", "selectDomainTypeWildcardName": "Wildcard Domain", - "selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Single Domain", "selectType": "Select a type", "actions": "Actions", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Expand", "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", - "domainPickerEnterDomain": "Enter your domain", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", "domainPickerDescription": "Enter the full domain of the resource to see available options.", "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Checking availability...", - "domainPickerNoMatchingDomains": "No matching domains found for \"{userInput}\". Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Organization Domains", "domainPickerProvidedDomains": "Provided Domains", "domainPickerSubdomain": "Subdomain: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Name:", "createDomainValue": "Value:", "createDomainCnameRecords": "CNAME Records", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Record {number}", "createDomainTxtRecords": "TXT Records", "createDomainSaveTheseRecords": "Save These Records", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "DNS Propagation", "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", "resourcePortRequired": "Port number is required for non-HTTP resources", - "resourcePortNotAllowed": "Port number should not be set for HTTP resources" + "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 91b03160eae6d836e3f5ea7bd9cecc6a3d5001a8 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:14 -0700 Subject: [PATCH 35/64] New translations en-us.json (German) --- messages/de-DE.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index d139b61d..6a6a0ada 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Fehler beim Erstellen der Site", "siteErrorCreateKeyPair": "Schlüsselpaar oder Standardwerte nicht gefunden", "siteErrorCreateDefaults": "Standardwerte der Site nicht gefunden", - "siteNameDescription": "Dies ist der Anzeigename für die Site.", "method": "Methode", "siteMethodDescription": "So werden Verbindungen freigegeben.", "siteLearnNewt": "Wie du Newt auf deinem System installieren kannst", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Alle Benutzer", "sidebarIdentityProviders": "Identitätsanbieter", "sidebarLicense": "Lizenz", - "sidebarClients": "Kunden", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", "enableDockerSocket": "Docker Socket aktivieren", "enableDockerSocketDescription": "Docker Socket-Erkennung aktivieren, um Container-Informationen zu befüllen. Socket-Pfad muss Newt bereitgestellt werden.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Einzelne Domain (CNAME)", "selectDomainTypeCnameDescription": "Nur diese spezifische Domain. Verwenden Sie dies für einzelne Subdomains oder spezifische Domaineinträge.", "selectDomainTypeWildcardName": "Wildcard-Domain", - "selectDomainTypeWildcardDescription": "Diese Domain und ihre erste Ebene der Subdomains.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Einzelne Domain", "selectType": "Typ auswählen", "actions": "Aktionen", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Erweitern", "newtUpdateAvailable": "Update verfügbar", "newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.", - "domainPickerEnterDomain": "Geben Sie Ihre Domain ein", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, oder einfach myapp", "domainPickerDescription": "Geben Sie die vollständige Domäne der Ressource ein, um verfügbare Optionen zu sehen.", "domainPickerDescriptionSaas": "Geben Sie eine vollständige Domäne, Subdomäne oder einfach einen Namen ein, um verfügbare Optionen zu sehen", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Verfügbarkeit prüfen...", - "domainPickerNoMatchingDomains": "Keine passenden Domains für \"{userInput}\" gefunden. Versuchen Sie es mit einer anderen Domain oder überprüfen Sie die Domain-Einstellungen Ihrer Organisation.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Organisations-Domains", "domainPickerProvidedDomains": "Bereitgestellte Domains", "domainPickerSubdomain": "Subdomain: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Name:", "createDomainValue": "Wert:", "createDomainCnameRecords": "CNAME-Einträge", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Eintrag {number}", "createDomainTxtRecords": "TXT-Einträge", "createDomainSaveTheseRecords": "Diese Einträge speichern", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "DNS-Verbreitung", "createDomainDnsPropagationDescription": "Es kann einige Zeit dauern, bis DNS-Änderungen im Internet verbreitet werden. Dies kann je nach Ihrem DNS-Provider und den TTL-Einstellungen von einigen Minuten bis zu 48 Stunden dauern.", "resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich", - "resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden" + "resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 3c6423d444838264c4ff03ce704fbe45aff85729 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:15 -0700 Subject: [PATCH 36/64] New translations en-us.json (Italian) --- messages/it-IT.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index ce13ab23..42b6cbc0 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Errore nella creazione del sito", "siteErrorCreateKeyPair": "Coppia di chiavi o valori predefiniti del sito non trovati", "siteErrorCreateDefaults": "Predefiniti del sito non trovati", - "siteNameDescription": "Questo è il nome visualizzato per il sito.", "method": "Metodo", "siteMethodDescription": "Questo è il modo in cui esporrete le connessioni.", "siteLearnNewt": "Scopri come installare Newt sul tuo sistema", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Tutti Gli Utenti", "sidebarIdentityProviders": "Fornitori Di Identità", "sidebarLicense": "Licenza", - "sidebarClients": "Clienti", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domini", "enableDockerSocket": "Abilita Docker Socket", "enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. Il percorso del socket deve essere fornito a Newt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Dominio Singolo (CNAME)", "selectDomainTypeCnameDescription": "Solo questo dominio specifico. Usa questo per sottodomini individuali o specifiche voci di dominio.", "selectDomainTypeWildcardName": "Dominio Jolly", - "selectDomainTypeWildcardDescription": "Questo dominio e il suo primo livello di sottodomini.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Dominio Singolo", "selectType": "Seleziona un tipo", "actions": "Azioni", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Espandi", "newtUpdateAvailable": "Aggiornamento Disponibile", "newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.", - "domainPickerEnterDomain": "Inserisci il tuo dominio", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, o semplicemente myapp", "domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.", "domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Controllando la disponibilità...", - "domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato per \"{userInput}\". Prova un altro dominio o controlla le impostazioni del dominio della tua organizzazione.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Domini dell'Organizzazione", "domainPickerProvidedDomains": "Domini Forniti", "domainPickerSubdomain": "Sottodominio: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Nome:", "createDomainValue": "Valore:", "createDomainCnameRecords": "Record CNAME", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Record {number}", "createDomainTxtRecords": "Record TXT", "createDomainSaveTheseRecords": "Salva Questi Record", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "Propagazione DNS", "createDomainDnsPropagationDescription": "Le modifiche DNS possono richiedere del tempo per propagarsi in Internet. Questo può richiedere da pochi minuti a 48 ore, a seconda del tuo provider DNS e delle impostazioni TTL.", "resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP", - "resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP" + "resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From b1a27e9060d0ecefa05080774fdc606ee2014076 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:17 -0700 Subject: [PATCH 37/64] New translations en-us.json (Korean) --- messages/ko-KR.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 1406a624..923cf4b3 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -59,7 +59,6 @@ "siteErrorCreate": "사이트 생성 오류", "siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다", "siteErrorCreateDefaults": "사이트 기본값을 찾을 수 없습니다", - "siteNameDescription": "이것은 사이트의 표시 이름입니다.", "method": "방법", "siteMethodDescription": "이것이 연결을 노출하는 방법입니다.", "siteLearnNewt": "시스템에 Newt 설치하는 방법 배우기", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "모든 사용자", "sidebarIdentityProviders": "신원 공급자", "sidebarLicense": "라이선스", - "sidebarClients": "클라이언트", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "도메인", "enableDockerSocket": "Docker 소켓 활성화", "enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "단일 도메인 (CNAME)", "selectDomainTypeCnameDescription": "단일 하위 도메인 또는 특정 도메인 항목에 사용됩니다.", "selectDomainTypeWildcardName": "와일드카드 도메인", - "selectDomainTypeWildcardDescription": "이 도메인과 그 첫 번째 레벨의 하위 도메인입니다.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "단일 도메인", "selectType": "유형 선택", "actions": "작업", @@ -1196,7 +1195,7 @@ "sidebarExpand": "확장하기", "newtUpdateAvailable": "업데이트 가능", "newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.", - "domainPickerEnterDomain": "도메인 입력", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp", "domainPickerDescription": "Enter the full domain of the resource to see available options.", "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "가용성을 확인 중...", - "domainPickerNoMatchingDomains": "\"{userInput}\"에 해당하는 도메인을 찾을 수 없습니다. 다른 도메인을 시도하거나 조직의 도메인 설정을 확인하세요.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "조직 도메인", "domainPickerProvidedDomains": "제공된 도메인", "domainPickerSubdomain": "서브도메인: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "이름:", "createDomainValue": "값:", "createDomainCnameRecords": "CNAME 레코드", + "createDomainARecords": "A Records", "createDomainRecordNumber": "레코드 {number}", "createDomainTxtRecords": "TXT 레코드", "createDomainSaveTheseRecords": "이 레코드 저장", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "DNS 전파", "createDomainDnsPropagationDescription": "DNS 변경 사항은 인터넷 전체에 전파되는 데 시간이 걸립니다. DNS 제공자와 TTL 설정에 따라 몇 분에서 48시간까지 걸릴 수 있습니다.", "resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다", - "resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요" + "resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From f8622da7d4211b903d06ea30cb79ef4cdb1c901c Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:18 -0700 Subject: [PATCH 38/64] New translations en-us.json (Dutch) --- messages/nl-NL.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index e756e281..9772f203 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Fout bij maken site", "siteErrorCreateKeyPair": "Key pair of site standaard niet gevonden", "siteErrorCreateDefaults": "Standaardinstellingen niet gevonden", - "siteNameDescription": "Dit is de weergavenaam van de site.", "method": "Methode", "siteMethodDescription": "Op deze manier legt u verbindingen bloot.", "siteLearnNewt": "Leer hoe u Newt kunt installeren op uw systeem", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Alle gebruikers", "sidebarIdentityProviders": "Identiteit aanbieders", "sidebarLicense": "Licentie", - "sidebarClients": "Cliënten", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domeinen", "enableDockerSocket": "Docker Socket inschakelen", "enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Enkel domein (CNAME)", "selectDomainTypeCnameDescription": "Alleen dit specifieke domein. Gebruik dit voor individuele subdomeinen of specifieke domeinvermeldingen.", "selectDomainTypeWildcardName": "Wildcard Domein", - "selectDomainTypeWildcardDescription": "Dit domein en zijn eerste niveau van subdomeinen.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Enkel domein", "selectType": "Selecteer een type", "actions": "acties", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Uitklappen", "newtUpdateAvailable": "Update beschikbaar", "newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.", - "domainPickerEnterDomain": "Voer je domein in", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "mijnapp.voorbeeld.com, api.v1.mijndomein.com, of gewoon mijnapp", "domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.", "domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Beschikbaarheid controleren...", - "domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden voor \"{userInput}\". Probeer een ander domein of controleer de domeininstellingen van je organisatie.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Organisatiedomeinen", "domainPickerProvidedDomains": "Aangeboden domeinen", "domainPickerSubdomain": "Subdomein: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Naam:", "createDomainValue": "Waarde:", "createDomainCnameRecords": "CNAME-records", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Record {number}", "createDomainTxtRecords": "TXT-records", "createDomainSaveTheseRecords": "Deze records opslaan", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "DNS-propagatie", "createDomainDnsPropagationDescription": "DNS-wijzigingen kunnen enige tijd duren om over het internet te worden verspreid. Dit kan enkele minuten tot 48 uur duren, afhankelijk van je DNS-provider en TTL-instellingen.", "resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen", - "resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen" + "resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 17586c4559ae1dc5942a8c270290d4ccfcff2484 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:19 -0700 Subject: [PATCH 39/64] New translations en-us.json (Polish) --- messages/pl-PL.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 966dea6a..61933d09 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Błąd podczas tworzenia witryny", "siteErrorCreateKeyPair": "Nie znaleziono pary kluczy lub domyślnych ustawień witryny", "siteErrorCreateDefaults": "Nie znaleziono domyślnych ustawień witryny", - "siteNameDescription": "To jest wyświetlana nazwa witryny.", "method": "Metoda", "siteMethodDescription": "W ten sposób ujawnisz połączenia.", "siteLearnNewt": "Dowiedz się, jak zainstalować Newt w systemie", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Wszyscy użytkownicy", "sidebarIdentityProviders": "Dostawcy tożsamości", "sidebarLicense": "Licencja", - "sidebarClients": "Klienci", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domeny", "enableDockerSocket": "Włącz gniazdo dokera", "enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka gniazda musi być dostarczona do Newt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Pojedyncza domena (CNAME)", "selectDomainTypeCnameDescription": "Tylko ta pojedyncza domena. Użyj tego dla poszczególnych subdomen lub wpisów specyficznych dla domeny.", "selectDomainTypeWildcardName": "Domena wieloznaczna", - "selectDomainTypeWildcardDescription": "Ta domena i jej pierwsza warstwa subdomen.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Pojedyncza domena", "selectType": "Wybierz typ", "actions": "Akcje", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Rozwiń", "newtUpdateAvailable": "Dostępna aktualizacja", "newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.", - "domainPickerEnterDomain": "Wprowadź swoją domenę", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com lub po prostu myapp", "domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.", "domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Sprawdzanie dostępności...", - "domainPickerNoMatchingDomains": "Nie znaleziono żadnych pasujących domen dla \"{userInput}\". Spróbuj innej domeny lub sprawdź ustawienia domeny swojej organizacji.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Domeny organizacji", "domainPickerProvidedDomains": "Dostarczone domeny", "domainPickerSubdomain": "Subdomena: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Nazwa:", "createDomainValue": "Wartość:", "createDomainCnameRecords": "Rekordy CNAME", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Rekord {number}", "createDomainTxtRecords": "Rekordy TXT", "createDomainSaveTheseRecords": "Zapisz te rekordy", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "Propagacja DNS", "createDomainDnsPropagationDescription": "Zmiany DNS mogą zająć trochę czasu na rozpropagowanie się w Internecie. Może to potrwać od kilku minut do 48 godzin, w zależności od dostawcy DNS i ustawień TTL.", "resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP", - "resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP" + "resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 85182015626e2d8a2094259c857f0a1a3097a8b5 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:21 -0700 Subject: [PATCH 40/64] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 762689f9..f3ae05c6 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Erro ao criar site", "siteErrorCreateKeyPair": "Par de chaves ou padrões do site não encontrados", "siteErrorCreateDefaults": "Padrão do site não encontrado", - "siteNameDescription": "Este é o nome de exibição do site.", "method": "Método", "siteMethodDescription": "É assim que você irá expor as conexões.", "siteLearnNewt": "Saiba como instalar o Newt no seu sistema", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Todos os usuários", "sidebarIdentityProviders": "Provedores de identidade", "sidebarLicense": "Tipo:", - "sidebarClients": "Clientes", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domínios", "enableDockerSocket": "Habilitar Docker Socket", "enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao Newt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Domínio Único (CNAME)", "selectDomainTypeCnameDescription": "Apenas este domínio específico. Use isso para subdomínios individuais ou entradas de domínio específicas.", "selectDomainTypeWildcardName": "Domínio Coringa", - "selectDomainTypeWildcardDescription": "Este domínio e seu primeiro nível de subdomínios.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Domínio Único", "selectType": "Selecione um tipo", "actions": "Ações", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Expandir", "newtUpdateAvailable": "Nova Atualização Disponível", "newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.", - "domainPickerEnterDomain": "Insira seu domínio", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "meuapp.exemplo.com, api.v1.meudominio.com, ou apenas meuapp", "domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.", "domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Verificando disponibilidade...", - "domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado para \"{userInput}\". Tente um domínio diferente ou verifique as configurações de domínio da sua organização.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Domínios da Organização", "domainPickerProvidedDomains": "Domínios Fornecidos", "domainPickerSubdomain": "Subdomínio: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Nome:", "createDomainValue": "Valor:", "createDomainCnameRecords": "Registros CNAME", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Registrar {number}", "createDomainTxtRecords": "Registros TXT", "createDomainSaveTheseRecords": "Salvar Esses Registros", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "Propagação DNS", "createDomainDnsPropagationDescription": "Alterações no DNS podem levar algum tempo para se propagar pela internet. Pode levar de alguns minutos a 48 horas, dependendo do seu provedor de DNS e das configurações de TTL.", "resourcePortRequired": "Número da porta é obrigatório para recursos não-HTTP", - "resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP" + "resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 448442f92b54907eef0294c2ed8c69b6fcdda33f Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:23 -0700 Subject: [PATCH 41/64] New translations en-us.json (Turkish) --- messages/tr-TR.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index a547a44b..ab71f7ee 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -59,7 +59,6 @@ "siteErrorCreate": "Site oluşturulurken hata", "siteErrorCreateKeyPair": "Anahtar çifti veya site varsayılanları bulunamadı", "siteErrorCreateDefaults": "Site varsayılanları bulunamadı", - "siteNameDescription": "Bu, site için görünen addır.", "method": "Yöntem", "siteMethodDescription": "Bağlantıları nasıl açığa çıkaracağınız budur.", "siteLearnNewt": "Newt'i sisteminize nasıl kuracağınızı öğrenin", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Tüm Kullanıcılar", "sidebarIdentityProviders": "Kimlik Sağlayıcılar", "sidebarLicense": "Lisans", - "sidebarClients": "Müşteriler", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Alan Adları", "enableDockerSocket": "Docker Soketi Etkinleştir", "enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Tekil Alan Adı (CNAME)", "selectDomainTypeCnameDescription": "Sadece bu belirli alan adı. Bireysel alt alan adları veya belirli alan adı girişleri için bunu kullanın.", "selectDomainTypeWildcardName": "Wildcard Alan Adı", - "selectDomainTypeWildcardDescription": "Bu alan adı ve onun ilk alt alan düzeyi.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Tekil Alan Adı", "selectType": "Bir tür seçin", "actions": "İşlemler", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Genişlet", "newtUpdateAvailable": "Güncelleme Mevcut", "newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.", - "domainPickerEnterDomain": "Alan adınızı girin", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com veya sadece myapp", "domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.", "domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Kullanılabilirlik kontrol ediliyor...", - "domainPickerNoMatchingDomains": "\"{userInput}\" için uygun alan adı bulunamadı. Farklı bir alan adı deneyin veya organizasyonunuzun alan adı ayarlarını kontrol edin.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Organizasyon Alan Adları", "domainPickerProvidedDomains": "Sağlanan Alan Adları", "domainPickerSubdomain": "Alt Alan: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Ad:", "createDomainValue": "Değer:", "createDomainCnameRecords": "CNAME Kayıtları", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Kayıt {number}", "createDomainTxtRecords": "TXT Kayıtları", "createDomainSaveTheseRecords": "Bu Kayıtları Kaydet", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "DNS Yayılması", "createDomainDnsPropagationDescription": "DNS değişikliklerinin internet genelinde yayılması zaman alabilir. DNS sağlayıcınız ve TTL ayarlarına bağlı olarak bu birkaç dakika ile 48 saat arasında değişebilir.", "resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir", - "resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı" + "resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From b596f00ce55775ad0fc69a2e2330cb781a055501 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 28 Jul 2025 14:16:24 -0700 Subject: [PATCH 42/64] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 4f7d779e..39f95a7c 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -59,7 +59,6 @@ "siteErrorCreate": "创建站点出错", "siteErrorCreateKeyPair": "找不到密钥对或站点默认值", "siteErrorCreateDefaults": "未找到站点默认值", - "siteNameDescription": "这是站点的显示名称。", "method": "方法", "siteMethodDescription": "这是您将如何显示连接。", "siteLearnNewt": "学习如何在您的系统上安装 Newt", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "所有用户", "sidebarIdentityProviders": "身份提供商", "sidebarLicense": "证书", - "sidebarClients": "客户", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "域", "enableDockerSocket": "启用停靠套接字", "enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "单个域(CNAME)", "selectDomainTypeCnameDescription": "仅此特定域。用于单个子域或特定域条目。", "selectDomainTypeWildcardName": "通配符域", - "selectDomainTypeWildcardDescription": "此域及其第一级子域。", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "单个域", "selectType": "选择一个类型", "actions": "操作", @@ -1196,7 +1195,7 @@ "sidebarExpand": "展开", "newtUpdateAvailable": "更新可用", "newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。", - "domainPickerEnterDomain": "输入您的域", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp", "domainPickerDescription": "输入资源的完整域名以查看可用选项。", "domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "检查可用性...", - "domainPickerNoMatchingDomains": "未找到 \"{userInput}\" 的匹配域。尝试其他域或检查您组织的域设置。", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "组织域", "domainPickerProvidedDomains": "提供的域", "domainPickerSubdomain": "子域:{subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "名称:", "createDomainValue": "值:", "createDomainCnameRecords": "CNAME 记录", + "createDomainARecords": "A Records", "createDomainRecordNumber": "记录 {number}", "createDomainTxtRecords": "TXT 记录", "createDomainSaveTheseRecords": "保存这些记录", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "DNS 传播", "createDomainDnsPropagationDescription": "DNS 更改可能需要一些时间才能在互联网上传播。这可能需要从几分钟到 48 小时,具体取决于您的 DNS 提供商和 TTL 设置。", "resourcePortRequired": "非 HTTP 资源必须输入端口号", - "resourcePortNotAllowed": "HTTP 资源不应设置端口号" + "resourcePortNotAllowed": "HTTP 资源不应设置端口号", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From adc0a81592b5827cdfee56af695c9cb3b16c0ed4 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Jul 2025 15:34:56 -0700 Subject: [PATCH 43/64] delete org domains and resources on org delete --- server/routers/org/deleteOrg.ts | 58 ++++++++++++++++++++++++++------- src/components/DomainPicker.tsx | 5 ++- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index 2f4ddf9e..76e2ad79 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -1,14 +1,8 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, resources } from "@server/db"; -import { - newts, - newtSessions, - orgs, - sites, - userActions -} from "@server/db"; -import { eq } from "drizzle-orm"; +import { db, domains, orgDomains, resources } from "@server/db"; +import { newts, newtSessions, orgs, sites, userActions } from "@server/db"; +import { eq, and, inArray, sql } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -126,8 +120,45 @@ export async function deleteOrg( } } - await trx.delete(orgs).where(eq(orgs.orgId, orgId)); + const allOrgDomains = await trx + .select() + .from(orgDomains) + .innerJoin(domains, eq(domains.domainId, orgDomains.domainId)) + .where( + and( + eq(orgDomains.orgId, orgId), + eq(domains.configManaged, false) + ) + ); + + // For each domain, check if it belongs to multiple organizations + const domainIdsToDelete: string[] = []; + for (const orgDomain of allOrgDomains) { + const domainId = orgDomain.domains.domainId; + + // Count how many organizations this domain belongs to + const orgCount = await trx + .select({ count: sql`count(*)` }) + .from(orgDomains) + .where(eq(orgDomains.domainId, domainId)); + + // Only delete the domain if it belongs to exactly 1 organization (the one being deleted) + if (orgCount[0].count === 1) { + domainIdsToDelete.push(domainId); + } + } + + // Delete domains that belong exclusively to this organization + if (domainIdsToDelete.length > 0) { + await trx + .delete(domains) + .where(inArray(domains.domainId, domainIdsToDelete)); + } + + // Delete resources await trx.delete(resources).where(eq(resources.orgId, orgId)); + + await trx.delete(orgs).where(eq(orgs.orgId, orgId)); }); // Send termination messages outside of transaction to prevent blocking @@ -137,8 +168,11 @@ export async function deleteOrg( data: {} }; // Don't await this to prevent blocking the response - sendToClient(newtId, payload).catch(error => { - logger.error("Failed to send termination message to newt:", error); + sendToClient(newtId, payload).catch((error) => { + logger.error( + "Failed to send termination message to newt:", + error + ); }); } diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 28dbcdbd..5f4104ea 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -129,9 +129,6 @@ export default function DomainPicker({ if (!userInput.trim()) return options; - // Check if input is more than one level deep (contains multiple dots) - const isMultiLevel = (userInput.match(/\./g) || []).length > 1; - // Add organization domain options organizationDomains.forEach((orgDomain) => { if (orgDomain.type === "cname") { @@ -319,6 +316,8 @@ export default function DomainPicker({ "" ); setUserInput(validInput); + // Clear selection when input changes + setSelectedOption(null); }} />

From 80656f48e065ed7bc949f3566bf11834495f9ebf Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Jul 2025 17:18:51 -0700 Subject: [PATCH 44/64] Sqlite migration done --- server/lib/readConfigFile.ts | 2 +- server/setup/scriptsPg/1.8.0.ts | 25 +++++++++++++++++++++++++ server/setup/scriptsSqlite/1.8.0.ts | 3 ++- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 server/setup/scriptsPg/1.8.0.ts diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 63951876..42fcefd3 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -229,7 +229,7 @@ export const configSchema = z disable_local_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(), - enable_clients: z.boolean().optional() + enable_clients: z.boolean().optional().default(true), }) .optional(), dns: z diff --git a/server/setup/scriptsPg/1.8.0.ts b/server/setup/scriptsPg/1.8.0.ts new file mode 100644 index 00000000..43b2a996 --- /dev/null +++ b/server/setup/scriptsPg/1.8.0.ts @@ -0,0 +1,25 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; + +const version = "1.7.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql` + BEGIN; + + + COMMIT; + `); + + console.log(`Migrated database schema`); + } catch (e) { + console.log("Unable to migrate database schema"); + console.log(e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsSqlite/1.8.0.ts b/server/setup/scriptsSqlite/1.8.0.ts index efb4f68a..bcee3e8d 100644 --- a/server/setup/scriptsSqlite/1.8.0.ts +++ b/server/setup/scriptsSqlite/1.8.0.ts @@ -13,9 +13,10 @@ export default async function migration() { try { db.transaction(() => { db.exec(` + ALTER TABLE 'resources' ADD 'enableProxy' integer DEFAULT true; + ALTER TABLE 'sites' ADD 'remoteSubnets' text; ALTER TABLE 'user' ADD 'termsAcceptedTimestamp' text; ALTER TABLE 'user' ADD 'termsVersion' text; - ALTER TABLE 'sites' ADD 'remoteSubnets' text; `); })(); From 4d7e25f97bb4df93acaf70f8b1390befecc52d68 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Jul 2025 17:22:53 -0700 Subject: [PATCH 45/64] Complete migrations --- server/db/pg/schema.ts | 2 +- server/lib/consts.ts | 2 +- server/setup/migrationsPg.ts | 4 +++- server/setup/migrationsSqlite.ts | 2 ++ server/setup/scriptsPg/1.8.0.ts | 9 ++++++++- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 5709c9f8..b9228286 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -95,7 +95,7 @@ export const resources = pgTable("resources", { stickySession: boolean("stickySession").notNull().default(false), tlsServerName: varchar("tlsServerName"), setHostHeader: varchar("setHostHeader"), - enableProxy: boolean("enableProxy").notNull().default(true), + enableProxy: boolean("enableProxy").default(true), }); export const targets = pgTable("targets", { diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 70d4404a..cfe45620 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.7.3"; +export const APP_VERSION = "1.8.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 6996999c..07ece65b 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -7,6 +7,7 @@ import { __DIRNAME, APP_VERSION } from "@server/lib/consts"; import path from "path"; import m1 from "./scriptsPg/1.6.0"; import m2 from "./scriptsPg/1.7.0"; +import m3 from "./scriptsPg/1.8.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -14,7 +15,8 @@ import m2 from "./scriptsPg/1.7.0"; // Define the migration list with versions and their corresponding functions const migrations = [ { version: "1.6.0", run: m1 }, - { version: "1.7.0", run: m2 } + { version: "1.7.0", run: m2 }, + { version: "1.8.0", run: m3 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 9fd5a470..15dd28d2 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -24,6 +24,7 @@ import m19 from "./scriptsSqlite/1.3.0"; import m20 from "./scriptsSqlite/1.5.0"; import m21 from "./scriptsSqlite/1.6.0"; import m22 from "./scriptsSqlite/1.7.0"; +import m23 from "./scriptsSqlite/1.8.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -47,6 +48,7 @@ const migrations = [ { version: "1.5.0", run: m20 }, { version: "1.6.0", run: m21 }, { version: "1.7.0", run: m22 }, + { version: "1.8.0", run: m23 }, // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.8.0.ts b/server/setup/scriptsPg/1.8.0.ts index 43b2a996..c1891c12 100644 --- a/server/setup/scriptsPg/1.8.0.ts +++ b/server/setup/scriptsPg/1.8.0.ts @@ -9,7 +9,14 @@ export default async function migration() { try { await db.execute(sql` BEGIN; - + + ALTER TABLE "clients" ALTER COLUMN "bytesIn" SET DATA TYPE real; + ALTER TABLE "clients" ALTER COLUMN "bytesOut" SET DATA TYPE real; + ALTER TABLE "clientSession" ALTER COLUMN "expiresAt" SET DATA TYPE bigint; + ALTER TABLE "resources" ADD COLUMN "enableProxy" boolean DEFAULT true; + ALTER TABLE "sites" ADD COLUMN "remoteSubnets" text; + ALTER TABLE "user" ADD COLUMN "termsAcceptedTimestamp" varchar; + ALTER TABLE "user" ADD COLUMN "termsVersion" varchar; COMMIT; `); From d732c1a8459e62fa641350a38a27124c004c9c7d Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Jul 2025 17:32:15 -0700 Subject: [PATCH 46/64] Clean up migrations --- server/setup/scriptsPg/1.8.0.ts | 4 ++-- server/setup/scriptsSqlite/1.8.0.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/setup/scriptsPg/1.8.0.ts b/server/setup/scriptsPg/1.8.0.ts index c1891c12..7c0b181b 100644 --- a/server/setup/scriptsPg/1.8.0.ts +++ b/server/setup/scriptsPg/1.8.0.ts @@ -1,7 +1,7 @@ import { db } from "@server/db/pg/driver"; import { sql } from "drizzle-orm"; -const version = "1.7.0"; +const version = "1.8.0"; export default async function migration() { console.log(`Running setup script ${version}...`); @@ -16,7 +16,7 @@ export default async function migration() { ALTER TABLE "resources" ADD COLUMN "enableProxy" boolean DEFAULT true; ALTER TABLE "sites" ADD COLUMN "remoteSubnets" text; ALTER TABLE "user" ADD COLUMN "termsAcceptedTimestamp" varchar; - ALTER TABLE "user" ADD COLUMN "termsVersion" varchar; + ALTER TABLE "user" ADD COLUMN "termsVersion" varchar; COMMIT; `); diff --git a/server/setup/scriptsSqlite/1.8.0.ts b/server/setup/scriptsSqlite/1.8.0.ts index bcee3e8d..f8ac7c95 100644 --- a/server/setup/scriptsSqlite/1.8.0.ts +++ b/server/setup/scriptsSqlite/1.8.0.ts @@ -5,7 +5,7 @@ import path from "path"; const version = "1.8.0"; export default async function migration() { - console.log("Running setup script ${version}..."); + console.log(`Running setup script ${version}...`); const location = path.join(APP_PATH, "db", "db.sqlite"); const db = new Database(location); @@ -13,7 +13,7 @@ export default async function migration() { try { db.transaction(() => { db.exec(` - ALTER TABLE 'resources' ADD 'enableProxy' integer DEFAULT true; + ALTER TABLE 'resources' ADD 'enableProxy' integer DEFAULT 1; ALTER TABLE 'sites' ADD 'remoteSubnets' text; ALTER TABLE 'user' ADD 'termsAcceptedTimestamp' text; ALTER TABLE 'user' ADD 'termsVersion' text; From 49981c4beeb7fc55dbf56a3dd512be453e65f6ca Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Jul 2025 18:34:01 -0700 Subject: [PATCH 47/64] Add 21820 to docker --- docker-compose.example.yml | 1 + install/config/docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 5a1b0a4e..c7c068f0 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -31,6 +31,7 @@ services: - SYS_MODULE ports: - 51820:51820/udp + - 21820:21820/udp - 443:443 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 35319dd0..4ce31e41 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -31,6 +31,7 @@ services: - SYS_MODULE ports: - 51820:51820/udp + - 21820:21820/udp - 443:443 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode {{end}} From 66f90a542ab252b2488457d80b9efa94527f031f Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Jul 2025 18:34:23 -0700 Subject: [PATCH 48/64] Rename to pg --- docker-compose.pgr.yml => docker-compose.pg.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker-compose.pgr.yml => docker-compose.pg.yml (100%) diff --git a/docker-compose.pgr.yml b/docker-compose.pg.yml similarity index 100% rename from docker-compose.pgr.yml rename to docker-compose.pg.yml From 35823d575135a9e565c028f787e875288bcc893e Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Jul 2025 22:40:27 -0700 Subject: [PATCH 49/64] Fix adding sites to client --- server/routers/client/updateClient.ts | 160 +++++++++++++++++- server/routers/gerbil/peers.ts | 3 +- server/routers/gerbil/updateHolePunch.ts | 142 +++++++++++++--- .../routers/olm/handleOlmRegisterMessage.ts | 18 +- .../[orgId]/settings/clients/create/page.tsx | 14 +- 5 files changed, 293 insertions(+), 44 deletions(-) diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 73c67d53..0dd75186 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, exitNodes, sites } from "@server/db"; import { clients, clientSites } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -17,6 +17,7 @@ import { addPeer as olmAddPeer, deletePeer as olmDeletePeer } from "../olm/peers"; +import axios from "axios"; const updateClientParamsSchema = z .object({ @@ -53,6 +54,11 @@ registry.registerPath({ responses: {} }); +interface PeerDestination { + destinationIP: string; + destinationPort: number; +} + export async function updateClient( req: Request, res: Response, @@ -124,15 +130,22 @@ export async function updateClient( ); for (const siteId of sitesAdded) { if (!client.subnet || !client.pubKey || !client.endpoint) { - logger.debug("Client subnet, pubKey or endpoint is not set"); + logger.debug( + "Client subnet, pubKey or endpoint is not set" + ); continue; } + // TODO: WE NEED TO HANDLE THIS BETTER. RIGHT NOW WE ARE JUST GUESSING BASED ON THE OTHER SITES + // BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS + const isRelayed = true; + const site = await newtAddPeer(siteId, { publicKey: client.pubKey, allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: client.endpoint + endpoint: isRelayed ? "" : client.endpoint }); + if (!site) { logger.debug("Failed to add peer to newt - missing site"); continue; @@ -142,9 +155,45 @@ export async function updateClient( logger.debug("Site endpoint or publicKey is not set"); continue; } + + let endpoint; + + if (isRelayed) { + if (!site.exitNodeId) { + logger.warn( + `Site ${site.siteId} has no exit node, skipping` + ); + return null; + } + + // get the exit node for the site + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, site.exitNodeId)) + .limit(1); + + if (!exitNode) { + logger.warn( + `Exit node not found for site ${site.siteId}` + ); + return null; + } + + endpoint = `${exitNode.endpoint}:21820`; + } else { + if (!endpoint) { + logger.warn( + `Site ${site.siteId} has no endpoint, skipping` + ); + return null; + } + endpoint = site.endpoint; + } + await olmAddPeer(client.clientId, { - siteId: siteId, - endpoint: site.endpoint, + siteId: site.siteId, + endpoint: endpoint, publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort, @@ -171,7 +220,11 @@ export async function updateClient( logger.debug("Site endpoint or publicKey is not set"); continue; } - await olmDeletePeer(client.clientId, site.siteId, site.publicKey); + await olmDeletePeer( + client.clientId, + site.siteId, + site.publicKey + ); } } @@ -202,6 +255,101 @@ export async function updateClient( } } + if (client.endpoint) { + // get all sites for this client and join with exit nodes with site.exitNodeId + const sitesData = await db + .select() + .from(sites) + .innerJoin( + clientSites, + eq(sites.siteId, clientSites.siteId) + ) + .leftJoin( + exitNodes, + eq(sites.exitNodeId, exitNodes.exitNodeId) + ) + .where(eq(clientSites.clientId, client.clientId)); + + let exitNodeDestinations: { + reachableAt: string; + destinations: PeerDestination[]; + }[] = []; + + for (const site of sitesData) { + if (!site.sites.subnet) { + logger.warn( + `Site ${site.sites.siteId} has no subnet, skipping` + ); + continue; + } + // find the destinations in the array + let destinations = exitNodeDestinations.find( + (d) => d.reachableAt === site.exitNodes?.reachableAt + ); + + if (!destinations) { + destinations = { + reachableAt: site.exitNodes?.reachableAt || "", + destinations: [ + { + destinationIP: + site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 + } + ] + }; + } else { + // add to the existing destinations + destinations.destinations.push({ + destinationIP: site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 + }); + } + + // update it in the array + exitNodeDestinations = exitNodeDestinations.filter( + (d) => d.reachableAt !== site.exitNodes?.reachableAt + ); + exitNodeDestinations.push(destinations); + } + + for (const destination of exitNodeDestinations) { + try { + logger.info( + `Updating destinations for exit node at ${destination.reachableAt}` + ); + const payload = { + sourceIp: client.endpoint?.split(":")[0] || "", + sourcePort: parseInt(client.endpoint?.split(":")[1]) || 0, + destinations: destination.destinations + }; + logger.info( + `Payload for update-destinations: ${JSON.stringify(payload, null, 2)}` + ); + const response = await axios.post( + `${destination.reachableAt}/update-destinations`, + payload, + { + headers: { + "Content-Type": "application/json" + } + } + ); + + logger.info("Destinations updated:", { + peer: response.data.status + }); + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}` + ); + } + throw error; + } + } + } + // Fetch the updated client const [updatedClient] = await trx .select() diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts index ce378ad4..70c56e04 100644 --- a/server/routers/gerbil/peers.ts +++ b/server/routers/gerbil/peers.ts @@ -8,7 +8,7 @@ export async function addPeer(exitNodeId: number, peer: { publicKey: string; allowedIps: string[]; }) { - + logger.info(`Adding peer with public key ${peer.publicKey} to exit node ${exitNodeId}`); const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1); if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); @@ -35,6 +35,7 @@ export async function addPeer(exitNodeId: number, peer: { } export async function deletePeer(exitNodeId: number, publicKey: string) { + logger.info(`Deleting peer with public key ${publicKey} from exit node ${exitNodeId}`); const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1); if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 4910738e..6d64249c 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { clients, newts, olms, Site, sites, clientSites } from "@server/db"; +import { clients, newts, olms, Site, sites, clientSites, exitNodes } from "@server/db"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; @@ -9,6 +9,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { validateNewtSessionToken } from "@server/auth/sessions/newt"; import { validateOlmSessionToken } from "@server/auth/sessions/olm"; +import axios from "axios"; // Define Zod schema for request validation const updateHolePunchSchema = z.object({ @@ -17,7 +18,8 @@ const updateHolePunchSchema = z.object({ token: z.string(), ip: z.string(), port: z.number(), - timestamp: z.number() + timestamp: z.number(), + reachableAt: z.string().optional() }); // New response type with multi-peer destination support @@ -43,7 +45,7 @@ export async function updateHolePunch( ); } - const { olmId, newtId, ip, port, timestamp, token } = parsedParams.data; + const { olmId, newtId, ip, port, timestamp, token, reachableAt } = parsedParams.data; let currentSiteId: number | undefined; let destinations: PeerDestination[] = []; @@ -94,36 +96,126 @@ export async function updateHolePunch( ); } - // Get all sites that this client is connected to - const clientSitePairs = await db - .select() - .from(clientSites) - .where(eq(clientSites.clientId, client.clientId)); + // // Get all sites that this client is connected to + // const clientSitePairs = await db + // .select() + // .from(clientSites) + // .where(eq(clientSites.clientId, client.clientId)); - if (clientSitePairs.length === 0) { - logger.warn(`No sites found for client: ${client.clientId}`); - return next( - createHttpError(HttpCode.NOT_FOUND, "No sites found for client") - ); - } + // if (clientSitePairs.length === 0) { + // logger.warn(`No sites found for client: ${client.clientId}`); + // return next( + // createHttpError(HttpCode.NOT_FOUND, "No sites found for client") + // ); + // } - // Get all sites details - const siteIds = clientSitePairs.map(pair => pair.siteId); + // // Get all sites details + // const siteIds = clientSitePairs.map(pair => pair.siteId); - for (const siteId of siteIds) { - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)); + // for (const siteId of siteIds) { + // const [site] = await db + // .select() + // .from(sites) + // .where(eq(sites.siteId, siteId)); - if (site && site.subnet && site.listenPort) { - destinations.push({ - destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort + // if (site && site.subnet && site.listenPort) { + // destinations.push({ + // destinationIP: site.subnet.split("/")[0], + // destinationPort: site.listenPort + // }); + // } + // } + + // get all sites for this client and join with exit nodes with site.exitNodeId + const sitesData = await db + .select() + .from(sites) + .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) + .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) + .where(eq(clientSites.clientId, client.clientId)); + + let exitNodeDestinations: { + reachableAt: string; + destinations: PeerDestination[]; + }[] = []; + + for (const site of sitesData) { + if (!site.sites.subnet) { + logger.warn(`Site ${site.sites.siteId} has no subnet, skipping`); + continue; + } + // find the destinations in the array + let destinations = exitNodeDestinations.find( + (d) => d.reachableAt === site.exitNodes?.reachableAt + ); + + if (!destinations) { + destinations = { + reachableAt: site.exitNodes?.reachableAt || "", + destinations: [ + { + destinationIP: site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 + } + ] + }; + } else { + // add to the existing destinations + destinations.destinations.push({ + destinationIP: site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 }); } + + // update it in the array + exitNodeDestinations = exitNodeDestinations.filter( + (d) => d.reachableAt !== site.exitNodes?.reachableAt + ); + exitNodeDestinations.push(destinations); } + logger.debug(JSON.stringify(exitNodeDestinations, null, 2)); + + for (const destination of exitNodeDestinations) { + // if its the current exit node skip it because it is replying with the same data + if (reachableAt && destination.reachableAt == reachableAt) { + logger.debug(`Skipping update for reachableAt: ${reachableAt}`); + continue; + } + + try { + const response = await axios.post( + `${destination.reachableAt}/update-destinations`, + { + sourceIp: client.endpoint?.split(":")[0] || "", + sourcePort: client.endpoint?.split(":")[1] || 0, + destinations: destination.destinations + }, + { + headers: { + "Content-Type": "application/json" + } + } + ); + + logger.info("Destinations updated:", { + peer: response.data.status + }); + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}` + ); + } + throw error; + } + } + + // Send the desinations back to the origin + destinations = exitNodeDestinations.find( + (d) => d.reachableAt === reachableAt + )?.destinations || []; + } else if (newtId) { logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`); diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 8a73daff..32e4fe51 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -104,6 +104,14 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // Prepare an array to store site configurations let siteConfigurations = []; logger.debug(`Found ${sitesData.length} sites for client ${client.clientId}`); + + if (sitesData.length === 0) { + sendToClient(olm.olmId, { + type: "olm/register/no-sites", + data: {} + }); + } + // Process each site for (const { sites: site } of sitesData) { if (!site.exitNodeId) { @@ -180,11 +188,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { }); } - // If we have no valid site configurations, don't send a connect message - if (siteConfigurations.length === 0) { - logger.warn("No valid site configurations found"); - return; - } + // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES + // if (siteConfigurations.length === 0) { + // logger.warn("No valid site configurations found"); + // return; + // } // Return connect message with all site configurations return { diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 88d2bef2..24fbe027 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -147,33 +147,33 @@ export default function Page() { mac: { "Apple Silicon (arm64)": [ `curl -L -o olm "https://github.com/fosrl/olm/releases/download/${version}/olm_darwin_arm64" && chmod +x ./olm`, - `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], "Intel x64 (amd64)": [ `curl -L -o olm "https://github.com/fosrl/olm/releases/download/${version}/olm_darwin_amd64" && chmod +x ./olm`, - `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ] }, linux: { amd64: [ `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_amd64" && chmod +x ./olm`, - `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], arm64: [ `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm64" && chmod +x ./olm`, - `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], arm32: [ `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm32" && chmod +x ./olm`, - `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], arm32v6: [ `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm32v6" && chmod +x ./olm`, - `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], riscv64: [ `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_riscv64" && chmod +x ./olm`, - `./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ] }, windows: { From 8fdb3ea63193ee560fc65f15e67ea6c49d48535b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Tue, 29 Jul 2025 10:46:01 -0700 Subject: [PATCH 50/64] hide favicon --- src/app/[orgId]/MemberResourcesPortal.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/[orgId]/MemberResourcesPortal.tsx b/src/app/[orgId]/MemberResourcesPortal.tsx index ad412b1e..4d3a7717 100644 --- a/src/app/[orgId]/MemberResourcesPortal.tsx +++ b/src/app/[orgId]/MemberResourcesPortal.tsx @@ -630,12 +630,6 @@ export default function MemberResourcesPortal({

-
- -
From 1cca06a27493e1ad570d190d8cf8a495bd530a5c Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 29 Jul 2025 23:09:49 -0700 Subject: [PATCH 51/64] Add note about wintun --- src/app/[orgId]/settings/clients/create/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 24fbe027..00b6b34c 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -178,7 +178,8 @@ export default function Page() { }, windows: { x64: [ - `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_amd64.exe"`, + `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`, + `# Run the installer to install olm and wintun`, `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}` ] } From 8a250d10114643deab3df51fadd620c2d93dddb0 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 30 Jul 2025 10:23:44 -0700 Subject: [PATCH 52/64] rm YC --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index f3214b6f..8c94815d 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,6 @@ _Pangolin tunnels your services to the internet so you can access anything from

-

- Launch YC: Pangolin – Open-source secure gateway to private networks -

- Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports. Preview From bb15af9954c2b10efa4568f815cc1a5732ff652b Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 30 Jul 2025 10:23:52 -0700 Subject: [PATCH 53/64] Add ports warn at start --- install/main.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/install/main.go b/install/main.go index 8160f2e9..9bb0c7e1 100644 --- a/install/main.go +++ b/install/main.go @@ -60,8 +60,23 @@ const ( ) func main() { + + // print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking + + fmt.Println("Welcome to the Pangolin installer!") + fmt.Println("This installer will help you set up Pangolin on your server.") + fmt.Println("") + fmt.Println("Please make sure you have the following prerequisites:") + fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.") + fmt.Println("- Point your domain to the VPS IP with A records.") + fmt.Println("") + fmt.Println("http://docs.fossorial.io/Getting%20Started/dns-networking") + fmt.Println("") + fmt.Println("Lets get started!") + fmt.Println("") + reader := bufio.NewReader(os.Stdin) - inputContainer := readString(reader, "Would you like to run Pangolin as docker or podman container?", "docker") + inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") chosenContainer := Docker if strings.EqualFold(inputContainer, "docker") { From d403bc86e3070dcd4520c67eda8c828f636f843c Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:24 -0700 Subject: [PATCH 54/64] New translations en-us.json (French) --- messages/fr-FR.json | 96 ++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index f41b3881..ccf9ccea 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "Tous les utilisateurs", "sidebarIdentityProviders": "Fournisseurs d'identité", "sidebarLicense": "Licence", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Clients (Bêta)", "sidebarDomains": "Domaines", "enableDockerSocket": "Activer Docker Socket", "enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du socket doit être fourni à Newt.", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Domaine unique (CNAME)", "selectDomainTypeCnameDescription": "Juste ce domaine spécifique. Utilisez ce paramètre pour des sous-domaines individuels ou des entrées de domaine spécifiques.", "selectDomainTypeWildcardName": "Domaine Générique", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Ce domaine et ses sous-domaines.", "domainDelegation": "Domaine Unique", "selectType": "Sélectionnez un type", "actions": "Actions", @@ -1195,7 +1195,7 @@ "sidebarExpand": "Développer", "newtUpdateAvailable": "Mise à jour disponible", "newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "Domaine", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, ou simplement myapp", "domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.", "domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Vérification de la disponibilité...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé. Essayez un autre domaine ou vérifiez les paramètres de domaine de votre organisation.", "domainPickerOrganizationDomains": "Domaines de l'organisation", "domainPickerProvidedDomains": "Domaines fournis", "domainPickerSubdomain": "Sous-domaine : {subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "Nom :", "createDomainValue": "Valeur :", "createDomainCnameRecords": "Enregistrements CNAME", - "createDomainARecords": "A Records", + "createDomainARecords": "Enregistrements A", "createDomainRecordNumber": "Enregistrement {number}", "createDomainTxtRecords": "Enregistrements TXT", "createDomainSaveTheseRecords": "Enregistrez ces enregistrements", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "Le numéro de port est requis pour les ressources non-HTTP", "resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Je suis d'accord avec", + "termsOfService": "les conditions d'utilisation", + "and": "et", + "privacyPolicy": "la politique de confidentialité" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "siteRequired": "Le site est requis.", + "olmTunnel": "Tunnel Olm", + "olmTunnelDescription": "Utilisez Olm pour la connectivité client", + "errorCreatingClient": "Erreur lors de la création du client", + "clientDefaultsNotFound": "Les paramètres par défaut du client sont introuvables", + "createClient": "Créer un client", + "createClientDescription": "Créez un nouveau client pour vous connecter à vos sites", + "seeAllClients": "Voir tous les clients", + "clientInformation": "Informations client", + "clientNamePlaceholder": "Nom du client", + "address": "Adresse", + "subnetPlaceholder": "Sous-réseau", + "addressDescription": "L'adresse que ce client utilisera pour la connectivité", + "selectSites": "Sélectionner des sites", + "sitesDescription": "Le client aura une connectivité vers les sites sélectionnés", + "clientInstallOlm": "Installer Olm", + "clientInstallOlmDescription": "Faites fonctionner Olm sur votre système", + "clientOlmCredentials": "Identifiants Olm", + "clientOlmCredentialsDescription": "C'est ainsi qu'Olm s'authentifiera auprès du serveur", + "olmEndpoint": "Point de terminaison Olm", + "olmId": "ID Olm", + "olmSecretKey": "Clé secrète Olm", + "clientCredentialsSave": "Enregistrez vos identifiants", + "clientCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.", + "generalSettingsDescription": "Configurez les paramètres généraux pour ce client", + "clientUpdated": "Client mis à jour", + "clientUpdatedDescription": "Le client a été mis à jour.", + "clientUpdateFailed": "Échec de la mise à jour du client", + "clientUpdateError": "Une erreur s'est produite lors de la mise à jour du client.", + "sitesFetchFailed": "Échec de la récupération des sites", + "sitesFetchError": "Une erreur s'est produite lors de la récupération des sites.", + "olmErrorFetchReleases": "Une erreur s'est produite lors de la récupération des versions d'Olm.", + "olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version d'Olm.", + "remoteSubnets": "Sous-réseaux distants", + "enterCidrRange": "Entrez la plage CIDR", + "remoteSubnetsDescription": "Ajoutez des plages CIDR pouvant accéder à ce site à distance. Utilisez le format comme 10.0.0.0/24 ou 192.168.1.0/24.", + "resourceEnableProxy": "Activer le proxy public", + "resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.", + "externalProxyEnabled": "Proxy externe activé" } From 37eb14a01ae04fec60758da44723073b7c15cef7 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:25 -0700 Subject: [PATCH 55/64] New translations en-us.json (Spanish) --- messages/es-ES.json | 96 ++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index 62d5ab80..e1d289c8 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "Todos los usuarios", "sidebarIdentityProviders": "Proveedores de identidad", "sidebarLicense": "Licencia", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Clientes (Beta)", "sidebarDomains": "Dominios", "enableDockerSocket": "Habilitar conector Docker", "enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. La ruta del socket debe proporcionarse a Newt.", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Dominio único (CNAME)", "selectDomainTypeCnameDescription": "Solo este dominio específico. Úsalo para subdominios individuales o entradas específicas de dominio.", "selectDomainTypeWildcardName": "Dominio comodín", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Este dominio y sus subdominios.", "domainDelegation": "Dominio único", "selectType": "Selecciona un tipo", "actions": "Acciones", @@ -1195,7 +1195,7 @@ "sidebarExpand": "Expandir", "newtUpdateAvailable": "Nueva actualización disponible", "newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "Dominio", "domainPickerPlaceholder": "myapp.example.com, api.v1.miDominio.com, o solo myapp", "domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.", "domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Comprobando disponibilidad...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "No se encontraron dominios que coincidan. Intente con un dominio diferente o verifique la configuración de dominios de su organización.", "domainPickerOrganizationDomains": "Dominios de la organización", "domainPickerProvidedDomains": "Dominios proporcionados", "domainPickerSubdomain": "Subdominio: {subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "Nombre:", "createDomainValue": "Valor:", "createDomainCnameRecords": "Registros CNAME", - "createDomainARecords": "A Records", + "createDomainARecords": "Registros A", "createDomainRecordNumber": "Registro {number}", "createDomainTxtRecords": "Registros TXT", "createDomainSaveTheseRecords": "Guardar estos registros", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "Se requiere número de puerto para recursos no HTTP", "resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Estoy de acuerdo con los", + "termsOfService": "términos del servicio", + "and": "y", + "privacyPolicy": "política de privacidad" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "siteRequired": "El sitio es requerido.", + "olmTunnel": "Túnel Olm", + "olmTunnelDescription": "Usar Olm para la conectividad del cliente", + "errorCreatingClient": "Error al crear el cliente", + "clientDefaultsNotFound": "Configuración predeterminada del cliente no encontrada", + "createClient": "Crear cliente", + "createClientDescription": "Crear un cliente nuevo para conectar a sus sitios", + "seeAllClients": "Ver todos los clientes", + "clientInformation": "Información del cliente", + "clientNamePlaceholder": "Nombre del cliente", + "address": "Dirección", + "subnetPlaceholder": "Subred", + "addressDescription": "La dirección que este cliente utilizará para la conectividad", + "selectSites": "Seleccionar sitios", + "sitesDescription": "El cliente tendrá conectividad con los sitios seleccionados", + "clientInstallOlm": "Instalar Olm", + "clientInstallOlmDescription": "Obtén Olm funcionando en tu sistema", + "clientOlmCredentials": "Credenciales Olm", + "clientOlmCredentialsDescription": "Así es como Olm se autentificará con el servidor", + "olmEndpoint": "Punto final Olm", + "olmId": "ID de Olm", + "olmSecretKey": "Clave secreta de Olm", + "clientCredentialsSave": "Guarda tus credenciales", + "clientCredentialsSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.", + "generalSettingsDescription": "Configura la configuración general para este cliente", + "clientUpdated": "Cliente actualizado", + "clientUpdatedDescription": "El cliente ha sido actualizado.", + "clientUpdateFailed": "Error al actualizar el cliente", + "clientUpdateError": "Se ha producido un error al actualizar el cliente.", + "sitesFetchFailed": "Error al obtener los sitios", + "sitesFetchError": "Se ha producido un error al recuperar los sitios.", + "olmErrorFetchReleases": "Se ha producido un error al recuperar las versiones de Olm.", + "olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.", + "remoteSubnets": "Subredes remotas", + "enterCidrRange": "Ingresa el rango CIDR", + "remoteSubnetsDescription": "Agregue rangos CIDR que puedan acceder a este sitio de forma remota. Use un formato como 10.0.0.0/24 o 192.168.1.0/24.", + "resourceEnableProxy": "Habilitar proxy público", + "resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.", + "externalProxyEnabled": "Proxy externo habilitado" } From 4fb3435c296110fc5e97cefb8a51eab058056b58 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:27 -0700 Subject: [PATCH 56/64] New translations en-us.json (German) --- messages/de-DE.json | 90 ++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index 6a6a0ada..7cce81e5 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Einzelne Domain (CNAME)", "selectDomainTypeCnameDescription": "Nur diese spezifische Domain. Verwenden Sie dies für einzelne Subdomains oder spezifische Domaineinträge.", "selectDomainTypeWildcardName": "Wildcard-Domain", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Diese Domain und ihre Subdomains.", "domainDelegation": "Einzelne Domain", "selectType": "Typ auswählen", "actions": "Aktionen", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Verfügbarkeit prüfen...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "Keine passenden Domains gefunden. Versuchen Sie es mit einer anderen Domain oder überprüfen Sie die Domain-Einstellungen Ihrer Organisation.", "domainPickerOrganizationDomains": "Organisations-Domains", "domainPickerProvidedDomains": "Bereitgestellte Domains", "domainPickerSubdomain": "Subdomain: {subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "Name:", "createDomainValue": "Wert:", "createDomainCnameRecords": "CNAME-Einträge", - "createDomainARecords": "A Records", + "createDomainARecords": "A-Aufzeichnungen", "createDomainRecordNumber": "Eintrag {number}", "createDomainTxtRecords": "TXT-Einträge", "createDomainSaveTheseRecords": "Diese Einträge speichern", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich", "resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Ich stimme den", + "termsOfService": "Nutzungsbedingungen zu", + "and": "und", + "privacyPolicy": "Datenschutzrichtlinie" }, - "siteRequired": "Site is required.", + "siteRequired": "Site ist erforderlich.", "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung", + "errorCreatingClient": "Fehler beim Erstellen des Clients", + "clientDefaultsNotFound": "Kundenvorgaben nicht gefunden", + "createClient": "Client erstellen", + "createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Sites.", + "seeAllClients": "Alle Clients anzeigen", + "clientInformation": "Kundeninformationen", + "clientNamePlaceholder": "Kundenname", + "address": "Adresse", + "subnetPlaceholder": "Subnetz", + "addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.", + "selectSites": "Sites auswählen", + "sitesDescription": "Der Client wird zu den ausgewählten Sites eine Verbindung haben.", + "clientInstallOlm": "Olm installieren", + "clientInstallOlmDescription": "Olm auf Ihrem System zum Laufen bringen", + "clientOlmCredentials": "Olm-Zugangsdaten", + "clientOlmCredentialsDescription": "So authentifiziert sich Olm beim Server", + "olmEndpoint": "Olm-Endpunkt", + "olmId": "Olm-ID", + "olmSecretKey": "Olm-Geheimschlüssel", + "clientCredentialsSave": "Speichern Sie Ihre Zugangsdaten", + "clientCredentialsSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.", + "generalSettingsDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diesen Client", + "clientUpdated": "Client aktualisiert", + "clientUpdatedDescription": "Der Client wurde aktualisiert.", + "clientUpdateFailed": "Fehler beim Aktualisieren des Clients", + "clientUpdateError": "Beim Aktualisieren des Clients ist ein Fehler aufgetreten.", + "sitesFetchFailed": "Fehler beim Abrufen von Sites", + "sitesFetchError": "Beim Abrufen von Sites ist ein Fehler aufgetreten.", + "olmErrorFetchReleases": "Beim Abrufen von Olm-Veröffentlichungen ist ein Fehler aufgetreten.", + "olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.", + "remoteSubnets": "Remote-Subnetze", + "enterCidrRange": "Geben Sie den CIDR-Bereich ein", + "remoteSubnetsDescription": "Fügen Sie CIDR-Bereiche hinzu, die aus der Ferne auf diese Site zugreifen können. Verwenden Sie das Format wie 10.0.0.0/24 oder 192.168.1.0/24.", + "resourceEnableProxy": "Öffentlichen Proxy aktivieren", + "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", + "externalProxyEnabled": "Externer Proxy aktiviert" } From ed20ed592f0361a572048ac45f135610d38e7041 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:28 -0700 Subject: [PATCH 57/64] New translations en-us.json (Italian) --- messages/it-IT.json | 94 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index 42b6cbc0..00fff828 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "Tutti Gli Utenti", "sidebarIdentityProviders": "Fornitori Di Identità", "sidebarLicense": "Licenza", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Clienti (Beta)", "sidebarDomains": "Domini", "enableDockerSocket": "Abilita Docker Socket", "enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. Il percorso del socket deve essere fornito a Newt.", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Dominio Singolo (CNAME)", "selectDomainTypeCnameDescription": "Solo questo dominio specifico. Usa questo per sottodomini individuali o specifiche voci di dominio.", "selectDomainTypeWildcardName": "Dominio Jolly", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Questo dominio e i suoi sottodomini.", "domainDelegation": "Dominio Singolo", "selectType": "Seleziona un tipo", "actions": "Azioni", @@ -1195,7 +1195,7 @@ "sidebarExpand": "Espandi", "newtUpdateAvailable": "Aggiornamento Disponibile", "newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "Dominio", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, o semplicemente myapp", "domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.", "domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Controllando la disponibilità...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato. Prova un dominio diverso o verifica le impostazioni del dominio della tua organizzazione.", "domainPickerOrganizationDomains": "Domini dell'Organizzazione", "domainPickerProvidedDomains": "Domini Forniti", "domainPickerSubdomain": "Sottodominio: {subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "Nome:", "createDomainValue": "Valore:", "createDomainCnameRecords": "Record CNAME", - "createDomainARecords": "A Records", + "createDomainARecords": "Record A", "createDomainRecordNumber": "Record {number}", "createDomainTxtRecords": "Record TXT", "createDomainSaveTheseRecords": "Salva Questi Record", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP", "resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Accetto i", + "termsOfService": "termini di servizio", + "and": "e", + "privacyPolicy": "informativa sulla privacy" }, - "siteRequired": "Site is required.", + "siteRequired": "Il sito è richiesto.", "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "olmTunnelDescription": "Usa Olm per la connettività client", + "errorCreatingClient": "Errore nella creazione del client", + "clientDefaultsNotFound": "Impostazioni predefinite del client non trovate", + "createClient": "Crea Cliente", + "createClientDescription": "Crea un nuovo cliente per connettersi ai tuoi siti", + "seeAllClients": "Vedi Tutti i Clienti", + "clientInformation": "Informazioni sul Cliente", + "clientNamePlaceholder": "Nome Cliente", + "address": "Indirizzo", + "subnetPlaceholder": "Sottorete", + "addressDescription": "L'indirizzo che questo cliente utilizzerà per la connettività", + "selectSites": "Seleziona siti", + "sitesDescription": "Il cliente avrà connettività ai siti selezionati", + "clientInstallOlm": "Installa Olm", + "clientInstallOlmDescription": "Avvia Olm sul tuo sistema", + "clientOlmCredentials": "Credenziali Olm", + "clientOlmCredentialsDescription": "Ecco come Olm si autenticherà con il server", + "olmEndpoint": "Endpoint Olm", + "olmId": "ID Olm", + "olmSecretKey": "Chiave Segreta Olm", + "clientCredentialsSave": "Salva le Tue Credenziali", + "clientCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.", + "generalSettingsDescription": "Configura le impostazioni generali per questo cliente", + "clientUpdated": "Cliente aggiornato", + "clientUpdatedDescription": "Il cliente è stato aggiornato.", + "clientUpdateFailed": "Impossibile aggiornare il cliente", + "clientUpdateError": "Si è verificato un errore durante l'aggiornamento del cliente.", + "sitesFetchFailed": "Impossibile recuperare i siti", + "sitesFetchError": "Si è verificato un errore durante il recupero dei siti.", + "olmErrorFetchReleases": "Si è verificato un errore durante il recupero delle versioni di Olm.", + "olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.", + "remoteSubnets": "Sottoreti Remote", + "enterCidrRange": "Inserisci l'intervallo CIDR", + "remoteSubnetsDescription": "Aggiungi intervalli CIDR che possono accedere a questo sito da remoto. Usa il formato come 10.0.0.0/24 o 192.168.1.0/24.", + "resourceEnableProxy": "Abilita Proxy Pubblico", + "resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.", + "externalProxyEnabled": "Proxy Esterno Abilitato" } From 48ff1ece1657c273e288e7780d10599ea0eae581 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:30 -0700 Subject: [PATCH 58/64] New translations en-us.json (Dutch) --- messages/nl-NL.json | 88 ++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 9772f203..14cfe9ac 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "Alle gebruikers", "sidebarIdentityProviders": "Identiteit aanbieders", "sidebarLicense": "Licentie", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Clients (Bèta)", "sidebarDomains": "Domeinen", "enableDockerSocket": "Docker Socket inschakelen", "enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Enkel domein (CNAME)", "selectDomainTypeCnameDescription": "Alleen dit specifieke domein. Gebruik dit voor individuele subdomeinen of specifieke domeinvermeldingen.", "selectDomainTypeWildcardName": "Wildcard Domein", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Dit domein en zijn subdomeinen.", "domainDelegation": "Enkel domein", "selectType": "Selecteer een type", "actions": "acties", @@ -1195,7 +1195,7 @@ "sidebarExpand": "Uitklappen", "newtUpdateAvailable": "Update beschikbaar", "newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "Domein", "domainPickerPlaceholder": "mijnapp.voorbeeld.com, api.v1.mijndomein.com, of gewoon mijnapp", "domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.", "domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Beschikbaarheid controleren...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden. Probeer een ander domein of controleer de domeininstellingen van uw organisatie.", "domainPickerOrganizationDomains": "Organisatiedomeinen", "domainPickerProvidedDomains": "Aangeboden domeinen", "domainPickerSubdomain": "Subdomein: {subdomain}", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen", "resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Ik ga akkoord met de", + "termsOfService": "servicevoorwaarden", + "and": "en", + "privacyPolicy": "privacybeleid" }, - "siteRequired": "Site is required.", + "siteRequired": "Site is vereist.", "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", + "olmTunnelDescription": "Gebruik Olm voor clientconnectiviteit", + "errorCreatingClient": "Fout bij het aanmaken van de client", + "clientDefaultsNotFound": "Standaardinstellingen van klant niet gevonden", + "createClient": "Client aanmaken", + "createClientDescription": "Maak een nieuwe client aan om verbinding te maken met uw sites", + "seeAllClients": "Alle clients bekijken", + "clientInformation": "Klantinformatie", + "clientNamePlaceholder": "Clientnaam", + "address": "Adres", "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", + "addressDescription": "Het adres dat deze client zal gebruiken voor connectiviteit", + "selectSites": "Selecteer sites", + "sitesDescription": "De client heeft connectiviteit met de geselecteerde sites", + "clientInstallOlm": "Installeer Olm", + "clientInstallOlmDescription": "Laat Olm draaien op uw systeem", + "clientOlmCredentials": "Olm inloggegevens", + "clientOlmCredentialsDescription": "Dit is hoe Olm zich bij de server zal verifiëren", + "olmEndpoint": "Olm Eindpunt", "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "olmSecretKey": "Olm Geheime Sleutel", + "clientCredentialsSave": "Uw referenties opslaan", + "clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.", + "generalSettingsDescription": "Configureer de algemene instellingen voor deze client", + "clientUpdated": "Klant bijgewerkt ", + "clientUpdatedDescription": "De client is bijgewerkt.", + "clientUpdateFailed": "Het bijwerken van de client is mislukt", + "clientUpdateError": "Er is een fout opgetreden tijdens het bijwerken van de client.", + "sitesFetchFailed": "Het ophalen van sites is mislukt", + "sitesFetchError": "Er is een fout opgetreden bij het ophalen van sites.", + "olmErrorFetchReleases": "Er is een fout opgetreden bij het ophalen van Olm releases.", + "olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.", + "remoteSubnets": "Externe Subnets", + "enterCidrRange": "Voer CIDR-bereik in", + "remoteSubnetsDescription": "Voeg CIDR-bereiken toe die deze site op afstand kunnen openen. Gebruik een format zoals 10.0.0.0/24 of 192.168.1.0/24.", + "resourceEnableProxy": "Openbare proxy inschakelen", + "resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.", + "externalProxyEnabled": "Externe Proxy Ingeschakeld" } From be5cb48dfe598850eade1843800c97506cdecf44 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:31 -0700 Subject: [PATCH 59/64] New translations en-us.json (Polish) --- messages/pl-PL.json | 96 ++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 61933d09..087a717e 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "Wszyscy użytkownicy", "sidebarIdentityProviders": "Dostawcy tożsamości", "sidebarLicense": "Licencja", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Klienci (Beta)", "sidebarDomains": "Domeny", "enableDockerSocket": "Włącz gniazdo dokera", "enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka gniazda musi być dostarczona do Newt.", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Pojedyncza domena (CNAME)", "selectDomainTypeCnameDescription": "Tylko ta pojedyncza domena. Użyj tego dla poszczególnych subdomen lub wpisów specyficznych dla domeny.", "selectDomainTypeWildcardName": "Domena wieloznaczna", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Ta domena i jej subdomeny.", "domainDelegation": "Pojedyncza domena", "selectType": "Wybierz typ", "actions": "Akcje", @@ -1195,7 +1195,7 @@ "sidebarExpand": "Rozwiń", "newtUpdateAvailable": "Dostępna aktualizacja", "newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "Domena", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com lub po prostu myapp", "domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.", "domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Sprawdzanie dostępności...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "Nie znaleziono pasujących domen. Spróbuj innej domeny lub sprawdź ustawienia domeny swojej organizacji.", "domainPickerOrganizationDomains": "Domeny organizacji", "domainPickerProvidedDomains": "Dostarczone domeny", "domainPickerSubdomain": "Subdomena: {subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "Nazwa:", "createDomainValue": "Wartość:", "createDomainCnameRecords": "Rekordy CNAME", - "createDomainARecords": "A Records", + "createDomainARecords": "Rekordy A", "createDomainRecordNumber": "Rekord {number}", "createDomainTxtRecords": "Rekordy TXT", "createDomainSaveTheseRecords": "Zapisz te rekordy", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP", "resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Zgadzam się z", + "termsOfService": "warunkami usługi", + "and": "oraz", + "privacyPolicy": "polityką prywatności" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "siteRequired": "Strona jest wymagana.", + "olmTunnel": "Tunel Olm", + "olmTunnelDescription": "Użyj Olm do łączności klienta", + "errorCreatingClient": "Błąd podczas tworzenia klienta", + "clientDefaultsNotFound": "Nie znaleziono domyślnych ustawień klienta", + "createClient": "Utwórz Klienta", + "createClientDescription": "Utwórz nowego klienta do łączenia się z Twoimi witrynami", + "seeAllClients": "Zobacz Wszystkich Klientów", + "clientInformation": "Informacje o Kliencie", + "clientNamePlaceholder": "Nazwa klienta", + "address": "Adres", + "subnetPlaceholder": "Podsieć", + "addressDescription": "Adres, którego ten klient będzie używać do łączności", + "selectSites": "Wybierz witryny", + "sitesDescription": "Klient będzie miał łączność z wybranymi witrynami", + "clientInstallOlm": "Zainstaluj Olm", + "clientInstallOlmDescription": "Uruchom Olm na swoim systemie", + "clientOlmCredentials": "Poświadczenia Olm", + "clientOlmCredentialsDescription": "To jest sposób, w jaki Olm będzie się uwierzytelniać z serwerem", + "olmEndpoint": "Punkt Końcowy Olm", + "olmId": "Identyfikator Olm", + "olmSecretKey": "Tajny Klucz Olm", + "clientCredentialsSave": "Zapisz swoje poświadczenia", + "clientCredentialsSaveDescription": "Będziesz mógł zobaczyć to tylko raz. Upewnij się, że skopiujesz go w bezpieczne miejsce.", + "generalSettingsDescription": "Skonfiguruj ogólne ustawienia dla tego klienta", + "clientUpdated": "Klient zaktualizowany", + "clientUpdatedDescription": "Klient został zaktualizowany.", + "clientUpdateFailed": "Nie udało się zaktualizować klienta", + "clientUpdateError": "Wystąpił błąd podczas aktualizacji klienta.", + "sitesFetchFailed": "Nie udało się pobrać witryn", + "sitesFetchError": "Wystąpił błąd podczas pobierania witryn.", + "olmErrorFetchReleases": "Wystąpił błąd podczas pobierania wydań Olm.", + "olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.", + "remoteSubnets": "Zdalne Podsieci", + "enterCidrRange": "Wprowadź zakres CIDR", + "remoteSubnetsDescription": "Dodaj zakresy CIDR, które mogą uzyskać zdalny dostęp do tej witryny. Użyj formatu takiego jak 10.0.0.0/24 lub 192.168.1.0/24.", + "resourceEnableProxy": "Włącz publiczny proxy", + "resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.", + "externalProxyEnabled": "Zewnętrzny Proxy Włączony" } From 5ec4481c9265274b881a97cd846df286effd13e9 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:32 -0700 Subject: [PATCH 60/64] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 94 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index f3ae05c6..5e46c51f 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "Todos os usuários", "sidebarIdentityProviders": "Provedores de identidade", "sidebarLicense": "Tipo:", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Clientes (Beta)", "sidebarDomains": "Domínios", "enableDockerSocket": "Habilitar Docker Socket", "enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao Newt.", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Domínio Único (CNAME)", "selectDomainTypeCnameDescription": "Apenas este domínio específico. Use isso para subdomínios individuais ou entradas de domínio específicas.", "selectDomainTypeWildcardName": "Domínio Coringa", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Este domínio e seus subdomínios.", "domainDelegation": "Domínio Único", "selectType": "Selecione um tipo", "actions": "Ações", @@ -1195,7 +1195,7 @@ "sidebarExpand": "Expandir", "newtUpdateAvailable": "Nova Atualização Disponível", "newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "Domínio", "domainPickerPlaceholder": "meuapp.exemplo.com, api.v1.meudominio.com, ou apenas meuapp", "domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.", "domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Verificando disponibilidade...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado. Tente um domínio diferente ou verifique as configurações do domínio da sua organização.", "domainPickerOrganizationDomains": "Domínios da Organização", "domainPickerProvidedDomains": "Domínios Fornecidos", "domainPickerSubdomain": "Subdomínio: {subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "Nome:", "createDomainValue": "Valor:", "createDomainCnameRecords": "Registros CNAME", - "createDomainARecords": "A Records", + "createDomainARecords": "Registros A", "createDomainRecordNumber": "Registrar {number}", "createDomainTxtRecords": "Registros TXT", "createDomainSaveTheseRecords": "Salvar Esses Registros", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "Número da porta é obrigatório para recursos não-HTTP", "resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Concordo com", + "termsOfService": "os termos de serviço", + "and": "e", + "privacyPolicy": "política de privacidade" }, - "siteRequired": "Site is required.", + "siteRequired": "Site é obrigatório.", "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "olmTunnelDescription": "Use Olm para conectividade do cliente", + "errorCreatingClient": "Erro ao criar cliente", + "clientDefaultsNotFound": "Padrões do cliente não encontrados", + "createClient": "Criar Cliente", + "createClientDescription": "Crie um novo cliente para conectar aos seus sites", + "seeAllClients": "Ver Todos os Clientes", + "clientInformation": "Informações do Cliente", + "clientNamePlaceholder": "Nome do cliente", + "address": "Endereço", + "subnetPlaceholder": "Sub-rede", + "addressDescription": "O endereço que este cliente usará para conectividade", + "selectSites": "Selecionar sites", + "sitesDescription": "O cliente terá conectividade com os sites selecionados", + "clientInstallOlm": "Instalar Olm", + "clientInstallOlmDescription": "Execute o Olm em seu sistema", + "clientOlmCredentials": "Credenciais Olm", + "clientOlmCredentialsDescription": "É assim que Olm se autenticará com o servidor", + "olmEndpoint": "Endpoint Olm", + "olmId": "ID Olm", + "olmSecretKey": "Chave Secreta Olm", + "clientCredentialsSave": "Salve suas Credenciais", + "clientCredentialsSaveDescription": "Você só poderá ver isto uma vez. Certifique-se de copiá-las para um local seguro.", + "generalSettingsDescription": "Configure as configurações gerais para este cliente", + "clientUpdated": "Cliente atualizado", + "clientUpdatedDescription": "O cliente foi atualizado.", + "clientUpdateFailed": "Falha ao atualizar cliente", + "clientUpdateError": "Ocorreu um erro ao atualizar o cliente.", + "sitesFetchFailed": "Falha ao buscar sites", + "sitesFetchError": "Ocorreu um erro ao buscar sites.", + "olmErrorFetchReleases": "Ocorreu um erro ao buscar lançamentos do Olm.", + "olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.", + "remoteSubnets": "Sub-redes Remotas", + "enterCidrRange": "Insira o intervalo CIDR", + "remoteSubnetsDescription": "Adicione intervalos CIDR que podem acessar este site remotamente. Use o formato como 10.0.0.0/24 ou 192.168.1.0/24.", + "resourceEnableProxy": "Ativar Proxy Público", + "resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.", + "externalProxyEnabled": "Proxy Externo Habilitado" } From bc3cb2c3c9470d2e12f9ea11d733014101ee327a Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:34 -0700 Subject: [PATCH 61/64] New translations en-us.json (Turkish) --- messages/tr-TR.json | 94 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index ab71f7ee..abd4b3e9 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "Tüm Kullanıcılar", "sidebarIdentityProviders": "Kimlik Sağlayıcılar", "sidebarLicense": "Lisans", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Müşteriler (Beta)", "sidebarDomains": "Alan Adları", "enableDockerSocket": "Docker Soketi Etkinleştir", "enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "Tekil Alan Adı (CNAME)", "selectDomainTypeCnameDescription": "Sadece bu belirli alan adı. Bireysel alt alan adları veya belirli alan adı girişleri için bunu kullanın.", "selectDomainTypeWildcardName": "Wildcard Alan Adı", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "Bu domain ve alt alan adları.", "domainDelegation": "Tekil Alan Adı", "selectType": "Bir tür seçin", "actions": "İşlemler", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Kullanılabilirlik kontrol ediliyor...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "Eşleşen domain bulunamadı. Farklı bir domain deneyin veya organizasyonunuzun domain ayarlarını kontrol edin.", "domainPickerOrganizationDomains": "Organizasyon Alan Adları", "domainPickerProvidedDomains": "Sağlanan Alan Adları", "domainPickerSubdomain": "Alt Alan: {subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "Ad:", "createDomainValue": "Değer:", "createDomainCnameRecords": "CNAME Kayıtları", - "createDomainARecords": "A Records", + "createDomainARecords": "A Kayıtları", "createDomainRecordNumber": "Kayıt {number}", "createDomainTxtRecords": "TXT Kayıtları", "createDomainSaveTheseRecords": "Bu Kayıtları Kaydet", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir", "resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Kabul ediyorum", + "termsOfService": "hizmet şartları", + "and": "ve", + "privacyPolicy": "gizlilik politikası" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "siteRequired": "Site gerekli.", + "olmTunnel": "Olm Tüneli", + "olmTunnelDescription": "Müşteri bağlantıları için Olm kullanın", + "errorCreatingClient": "Müşteri oluşturulurken hata oluştu", + "clientDefaultsNotFound": "Müşteri varsayılanları bulunamadı", + "createClient": "Müşteri Oluştur", + "createClientDescription": "Sitelerinize bağlanmak için yeni bir müşteri oluşturun", + "seeAllClients": "Tüm Müşterileri Gör", + "clientInformation": "Müşteri Bilgileri", + "clientNamePlaceholder": "Müşteri adı", + "address": "Adres", + "subnetPlaceholder": "Alt ağ", + "addressDescription": "Bu müşteri için bağlantıda kullanılacak adres", + "selectSites": "Siteleri seçin", + "sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır", + "clientInstallOlm": "Olm Yükle", + "clientInstallOlmDescription": "Sisteminizde Olm çalıştırın", + "clientOlmCredentials": "Olm Kimlik Bilgileri", + "clientOlmCredentialsDescription": "Bu, Olm'in sunucu ile kimlik doğrulaması yapacağı yöntemdir", + "olmEndpoint": "Olm Uç Noktası", + "olmId": "Olm Kimliği", + "olmSecretKey": "Olm Gizli Anahtarı", + "clientCredentialsSave": "Kimlik Bilgilerinizi Kaydedin", + "clientCredentialsSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.", + "generalSettingsDescription": "Bu müşteri için genel ayarları yapılandırın", + "clientUpdated": "Müşteri güncellendi", + "clientUpdatedDescription": "Müşteri güncellenmiştir.", + "clientUpdateFailed": "Müşteri güncellenemedi", + "clientUpdateError": "Müşteri güncellenirken bir hata oluştu.", + "sitesFetchFailed": "Siteler alınamadı", + "sitesFetchError": "Siteler alınırken bir hata oluştu.", + "olmErrorFetchReleases": "Olm yayınları alınırken bir hata oluştu.", + "olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.", + "remoteSubnets": "Uzak Alt Ağlar", + "enterCidrRange": "CIDR aralığını girin", + "remoteSubnetsDescription": "Bu siteye uzaktan erişebilecek CIDR aralıklarını ekleyin. 10.0.0.0/24 veya 192.168.1.0/24 gibi formatlar kullanın.", + "resourceEnableProxy": "Genel Proxy'i Etkinleştir", + "resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.", + "externalProxyEnabled": "Dış Proxy Etkinleştirildi" } From 31feabbec732db57d0424903595e5bf1fa9f15ba Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:35 -0700 Subject: [PATCH 62/64] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 94 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 39f95a7c..38da8715 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -1093,7 +1093,7 @@ "sidebarAllUsers": "所有用户", "sidebarIdentityProviders": "身份提供商", "sidebarLicense": "证书", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "客户端(测试版)", "sidebarDomains": "域", "enableDockerSocket": "启用停靠套接字", "enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。", @@ -1161,7 +1161,7 @@ "selectDomainTypeCnameName": "单个域(CNAME)", "selectDomainTypeCnameDescription": "仅此特定域。用于单个子域或特定域条目。", "selectDomainTypeWildcardName": "通配符域", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", + "selectDomainTypeWildcardDescription": "此域名及其子域名。", "domainDelegation": "单个域", "selectType": "选择一个类型", "actions": "操作", @@ -1195,7 +1195,7 @@ "sidebarExpand": "展开", "newtUpdateAvailable": "更新可用", "newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "域名", "domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp", "domainPickerDescription": "输入资源的完整域名以查看可用选项。", "domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。", @@ -1205,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "检查可用性...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "未找到匹配的域名。尝试不同的域名或检查您组织的域名设置。", "domainPickerOrganizationDomains": "组织域", "domainPickerProvidedDomains": "提供的域", "domainPickerSubdomain": "子域:{subdomain}", @@ -1265,7 +1265,7 @@ "createDomainName": "名称:", "createDomainValue": "值:", "createDomainCnameRecords": "CNAME 记录", - "createDomainARecords": "A Records", + "createDomainARecords": "A记录", "createDomainRecordNumber": "记录 {number}", "createDomainTxtRecords": "TXT 记录", "createDomainSaveTheseRecords": "保存这些记录", @@ -1275,48 +1275,48 @@ "resourcePortRequired": "非 HTTP 资源必须输入端口号", "resourcePortNotAllowed": "HTTP 资源不应设置端口号", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "我同意", + "termsOfService": "服务条款", + "and": "和", + "privacyPolicy": "隐私政策" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", + "siteRequired": "需要站点。", + "olmTunnel": "Olm 隧道", + "olmTunnelDescription": "使用 Olm 进行客户端连接", + "errorCreatingClient": "创建客户端出错", + "clientDefaultsNotFound": "未找到客户端默认值", + "createClient": "创建客户端", + "createClientDescription": "创建一个新客户端来连接您的站点", + "seeAllClients": "查看所有客户端", + "clientInformation": "客户端信息", + "clientNamePlaceholder": "客户端名称", + "address": "地址", + "subnetPlaceholder": "子网", + "addressDescription": "此客户端将用于连接的地址", + "selectSites": "选择站点", + "sitesDescription": "客户端将与所选站点进行连接", + "clientInstallOlm": "安装 Olm", + "clientInstallOlmDescription": "在您的系统上运行 Olm", + "clientOlmCredentials": "Olm 凭据", + "clientOlmCredentialsDescription": "这是 Olm 服务器的身份验证方式", + "olmEndpoint": "Olm 端点", "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "olmSecretKey": "Olm 私钥", + "clientCredentialsSave": "保存您的凭据", + "clientCredentialsSaveDescription": "该信息仅会显示一次,请确保将其复制到安全位置。", + "generalSettingsDescription": "配置此客户端的常规设置", + "clientUpdated": "客户端已更新", + "clientUpdatedDescription": "客户端已更新。", + "clientUpdateFailed": "更新客户端失败", + "clientUpdateError": "更新客户端时出错。", + "sitesFetchFailed": "获取站点失败", + "sitesFetchError": "获取站点时出错。", + "olmErrorFetchReleases": "获取 Olm 发布版本时出错。", + "olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。", + "remoteSubnets": "远程子网", + "enterCidrRange": "输入 CIDR 范围", + "remoteSubnetsDescription": "添加能远程访问此站点的 CIDR 范围。使用格式如 10.0.0.0/24 或 192.168.1.0/24。", + "resourceEnableProxy": "启用公共代理", + "resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。", + "externalProxyEnabled": "外部代理已启用" } From c49fe04750cb9433355f646b88cbbd069b4a8dbb Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 30 Jul 2025 15:18:36 -0700 Subject: [PATCH 63/64] New translations en-us.json (Russian) --- messages/ru-RU.json | 491 ++++++++++++++++++++++++-------------------- 1 file changed, 268 insertions(+), 223 deletions(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index d72ad989..5bbbf780 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -6,12 +6,12 @@ "setupOrgName": "Название организации", "orgDisplayName": "Это отображаемое имя вашей организации.", "orgId": "ID организации", - "setupIdentifierMessage": "Это уникальный идентификатор вашей организации. Он отличается от отображаемого имени.", + "setupIdentifierMessage": "Уникальный идентификатор вашей организации. Он задаётся отдельно от отображаемого имени.", "setupErrorIdentifier": "ID организации уже занят. Выберите другой.", "componentsErrorNoMemberCreate": "Вы пока не состоите ни в одной организации. Создайте организацию для начала работы.", "componentsErrorNoMember": "Вы пока не состоите ни в одной организации.", - "welcome": "Welcome!", - "welcomeTo": "Welcome to", + "welcome": "Добро пожаловать!", + "welcomeTo": "Добро пожаловать в", "componentsCreateOrg": "Создать организацию", "componentsMember": "Вы состоите в {count, plural, =0 {0 организациях} one {# организации} few {# организациях} many {# организациях} other {# организациях}}.", "componentsInvalidKey": "Обнаружены недействительные или просроченные лицензионные ключи. Соблюдайте условия лицензии для использования всех функций.", @@ -59,7 +59,6 @@ "siteErrorCreate": "Ошибка при создании сайта", "siteErrorCreateKeyPair": "Пара ключей или настройки сайта по умолчанию не найдены", "siteErrorCreateDefaults": "Настройки сайта по умолчанию не найдены", - "siteNameDescription": "Отображаемое имя сайта.", "method": "Метод", "siteMethodDescription": "Это способ, которым вы будете открывать соединения.", "siteLearnNewt": "Узнайте, как установить Newt в вашей системе", @@ -207,7 +206,7 @@ "orgGeneralSettings": "Настройки организации", "orgGeneralSettingsDescription": "Управляйте данными и конфигурацией вашей организации", "saveGeneralSettings": "Сохранить общие настройки", - "saveSettings": "Save Settings", + "saveSettings": "Сохранить настройки", "orgDangerZone": "Опасная зона", "orgDangerZoneDescription": "Будьте осторожны: удалив организацию, вы не сможете восстановить её.", "orgDelete": "Удалить организацию", @@ -646,53 +645,53 @@ "resourcePincodeProtection": "Защита PIN-кодом {status}", "resourcePincodeRemove": "PIN-код ресурса удалён", "resourcePincodeRemoveDescription": "PIN-код ресурса был успешно удалён", - "resourcePincodeSetup": "Resource PIN code set", - "resourcePincodeSetupDescription": "The resource pincode has been set successfully", - "resourcePincodeSetupTitle": "Set Pincode", - "resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource", - "resourceRoleDescription": "Admins can always access this resource.", - "resourceUsersRoles": "Users & Roles", - "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", - "resourceUsersRolesSubmit": "Save Users & Roles", - "resourceWhitelistSave": "Saved successfully", - "resourceWhitelistSaveDescription": "Whitelist settings have been saved", - "ssoUse": "Use Platform SSO", + "resourcePincodeSetup": "PIN-код ресурса установлен", + "resourcePincodeSetupDescription": "PIN-код ресурса был успешно установлен", + "resourcePincodeSetupTitle": "Установить PIN-код", + "resourcePincodeSetupTitleDescription": "Установите PIN-код для защиты этого ресурса", + "resourceRoleDescription": "Администраторы всегда имеют доступ к этому ресурсу.", + "resourceUsersRoles": "Пользователи и роли", + "resourceUsersRolesDescription": "Выберите пользователей и роли с доступом к этому ресурсу", + "resourceUsersRolesSubmit": "Сохранить пользователей и роли", + "resourceWhitelistSave": "Успешно сохранено", + "resourceWhitelistSaveDescription": "Настройки белого списка были сохранены", + "ssoUse": "Использовать Platform SSO", "ssoUseDescription": "Существующим пользователям нужно будет войти только один раз для всех ресурсов с включенной этой опцией.", - "proxyErrorInvalidPort": "Invalid port number", - "subdomainErrorInvalid": "Invalid subdomain", - "domainErrorFetch": "Error fetching domains", - "domainErrorFetchDescription": "An error occurred when fetching the domains", - "resourceErrorUpdate": "Failed to update resource", - "resourceErrorUpdateDescription": "An error occurred while updating the resource", - "resourceUpdated": "Resource updated", - "resourceUpdatedDescription": "The resource has been updated successfully", - "resourceErrorTransfer": "Failed to transfer resource", - "resourceErrorTransferDescription": "An error occurred while transferring the resource", - "resourceTransferred": "Resource transferred", - "resourceTransferredDescription": "The resource has been transferred successfully", - "resourceErrorToggle": "Failed to toggle resource", - "resourceErrorToggleDescription": "An error occurred while updating the resource", - "resourceVisibilityTitle": "Visibility", - "resourceVisibilityTitleDescription": "Completely enable or disable resource visibility", - "resourceGeneral": "General Settings", - "resourceGeneralDescription": "Configure the general settings for this resource", - "resourceEnable": "Enable Resource", - "resourceTransfer": "Transfer Resource", - "resourceTransferDescription": "Transfer this resource to a different site", - "resourceTransferSubmit": "Transfer Resource", - "siteDestination": "Destination Site", - "searchSites": "Search sites", - "accessRoleCreate": "Create Role", - "accessRoleCreateDescription": "Create a new role to group users and manage their permissions.", - "accessRoleCreateSubmit": "Create Role", - "accessRoleCreated": "Role created", - "accessRoleCreatedDescription": "The role has been successfully created.", - "accessRoleErrorCreate": "Failed to create role", - "accessRoleErrorCreateDescription": "An error occurred while creating the role.", - "accessRoleErrorNewRequired": "New role is required", - "accessRoleErrorRemove": "Failed to remove role", - "accessRoleErrorRemoveDescription": "An error occurred while removing the role.", - "accessRoleName": "Role Name", + "proxyErrorInvalidPort": "Неверный номер порта", + "subdomainErrorInvalid": "Неверный поддомен", + "domainErrorFetch": "Ошибка при получении доменов", + "domainErrorFetchDescription": "Произошла ошибка при получении доменов", + "resourceErrorUpdate": "Не удалось обновить ресурс", + "resourceErrorUpdateDescription": "Произошла ошибка при обновлении ресурса", + "resourceUpdated": "Ресурс обновлён", + "resourceUpdatedDescription": "Ресурс был успешно обновлён", + "resourceErrorTransfer": "Не удалось перенести ресурс", + "resourceErrorTransferDescription": "Произошла ошибка при переносе ресурса", + "resourceTransferred": "Ресурс перенесён", + "resourceTransferredDescription": "Ресурс был успешно перенесён", + "resourceErrorToggle": "Не удалось переключить ресурс", + "resourceErrorToggleDescription": "Произошла ошибка при обновлении ресурса", + "resourceVisibilityTitle": "Видимость", + "resourceVisibilityTitleDescription": "Включите или отключите видимость ресурса", + "resourceGeneral": "Общие настройки", + "resourceGeneralDescription": "Настройте общие параметры этого ресурса", + "resourceEnable": "Ресурс активен", + "resourceTransfer": "Перенести ресурс", + "resourceTransferDescription": "Перенесите этот ресурс на другой сайт", + "resourceTransferSubmit": "Перенести ресурс", + "siteDestination": "Новый сайт для ресурса", + "searchSites": "Поиск сайтов", + "accessRoleCreate": "Создание роли", + "accessRoleCreateDescription": "Создайте новую роль для группы пользователей и выдавайте им разрешения.", + "accessRoleCreateSubmit": "Создать роль", + "accessRoleCreated": "Роль создана", + "accessRoleCreatedDescription": "Роль была успешно создана.", + "accessRoleErrorCreate": "Не удалось создать роль", + "accessRoleErrorCreateDescription": "Произошла ошибка при создании роли.", + "accessRoleErrorNewRequired": "Новая роль обязательна", + "accessRoleErrorRemove": "Не удалось удалить роль", + "accessRoleErrorRemoveDescription": "Произошла ошибка при удалении роли.", + "accessRoleName": "Название роли", "accessRoleQuestionRemove": "Вы собираетесь удалить роль {name}. Это действие нельзя отменить.", "accessRoleRemove": "Удалить роль", "accessRoleRemoveDescription": "Удалить роль из организации", @@ -726,86 +725,86 @@ "idpSearch": "Поиск поставщиков удостоверений...", "idpAdd": "Добавить поставщика удостоверений", "idpClientIdRequired": "ID клиента обязателен.", - "idpClientSecretRequired": "Client Secret is required.", - "idpErrorAuthUrlInvalid": "Auth URL must be a valid URL.", - "idpErrorTokenUrlInvalid": "Token URL must be a valid URL.", - "idpPathRequired": "Identifier Path is required.", - "idpScopeRequired": "Scopes are required.", - "idpOidcDescription": "Configure an OpenID Connect identity provider", - "idpCreatedDescription": "Identity provider created successfully", - "idpCreate": "Create Identity Provider", - "idpCreateDescription": "Configure a new identity provider for user authentication", - "idpSeeAll": "See All Identity Providers", - "idpSettingsDescription": "Configure the basic information for your identity provider", - "idpDisplayName": "A display name for this identity provider", - "idpAutoProvisionUsers": "Auto Provision Users", + "idpClientSecretRequired": "Требуется секретный пароль клиента.", + "idpErrorAuthUrlInvalid": "URL авторизации должен быть корректным URL.", + "idpErrorTokenUrlInvalid": "URL токена должен быть корректным URL.", + "idpPathRequired": "Путь идентификатора обязателен.", + "idpScopeRequired": "Области действия обязательны.", + "idpOidcDescription": "Настройте поставщика удостоверений OpenID Connect", + "idpCreatedDescription": "Поставщик удостоверений успешно создан", + "idpCreate": "Создать поставщика удостоверений", + "idpCreateDescription": "Настройте нового поставщика удостоверений для аутентификации пользователей", + "idpSeeAll": "Посмотреть всех поставщиков удостоверений", + "idpSettingsDescription": "Настройте базовую информацию для вашего поставщика удостоверений", + "idpDisplayName": "Отображаемое имя для этого поставщика удостоверений", + "idpAutoProvisionUsers": "Автоматическое создание пользователей", "idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.", - "licenseBadge": "Professional", - "idpType": "Provider Type", - "idpTypeDescription": "Select the type of identity provider you want to configure", - "idpOidcConfigure": "OAuth2/OIDC Configuration", + "licenseBadge": "Профессиональная", + "idpType": "Тип поставщика", + "idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить", + "idpOidcConfigure": "Конфигурация OAuth2/OIDC", "idpOidcConfigureDescription": "Настройте конечные точки и учётные данные поставщика OAuth2/OIDC", - "idpClientId": "Client ID", - "idpClientIdDescription": "The OAuth2 client ID from your identity provider", - "idpClientSecret": "Client Secret", - "idpClientSecretDescription": "The OAuth2 client secret from your identity provider", - "idpAuthUrl": "Authorization URL", - "idpAuthUrlDescription": "The OAuth2 authorization endpoint URL", - "idpTokenUrl": "Token URL", - "idpTokenUrlDescription": "The OAuth2 token endpoint URL", - "idpOidcConfigureAlert": "Important Information", + "idpClientId": "ID клиента", + "idpClientIdDescription": "OAuth2 ID клиента от вашего поставщика удостоверений", + "idpClientSecret": "Секрет клиента", + "idpClientSecretDescription": "OAuth2 секрет клиента от вашего поставщика удостоверений", + "idpAuthUrl": "URL авторизации", + "idpAuthUrlDescription": "URL конечной точки авторизации OAuth2", + "idpTokenUrl": "URL токена", + "idpTokenUrlDescription": "URL конечной точки токена OAuth2", + "idpOidcConfigureAlert": "Важная информация", "idpOidcConfigureAlertDescription": "После создания поставщика удостоверений вам нужно будет настроить URL обратного вызова в настройках вашего поставщика удостоверений. URL обратного вызова будет предоставлен после успешного создания.", - "idpToken": "Token Configuration", - "idpTokenDescription": "Configure how to extract user information from the ID token", - "idpJmespathAbout": "About JMESPath", - "idpJmespathAboutDescription": "The paths below use JMESPath syntax to extract values from the ID token.", - "idpJmespathAboutDescriptionLink": "Learn more about JMESPath", - "idpJmespathLabel": "Identifier Path", - "idpJmespathLabelDescription": "The path to the user identifier in the ID token", - "idpJmespathEmailPathOptional": "Email Path (Optional)", - "idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token", - "idpJmespathNamePathOptional": "Name Path (Optional)", - "idpJmespathNamePathOptionalDescription": "The path to the user's name in the ID token", - "idpOidcConfigureScopes": "Scopes", - "idpOidcConfigureScopesDescription": "Space-separated list of OAuth2 scopes to request", - "idpSubmit": "Create Identity Provider", - "orgPolicies": "Organization Policies", + "idpToken": "Конфигурация токена", + "idpTokenDescription": "Настройте, как извлекать информацию о пользователе из ID токена", + "idpJmespathAbout": "О JMESPath", + "idpJmespathAboutDescription": "Пути ниже используют синтаксис JMESPath для извлечения значений из ID токена.", + "idpJmespathAboutDescriptionLink": "Узнать больше о JMESPath", + "idpJmespathLabel": "Путь идентификатора", + "idpJmespathLabelDescription": "Путь к идентификатору пользователя в ID токене", + "idpJmespathEmailPathOptional": "Путь к email (необязательно)", + "idpJmespathEmailPathOptionalDescription": "Путь к email пользователя в ID токене", + "idpJmespathNamePathOptional": "Путь к имени (необязательно)", + "idpJmespathNamePathOptionalDescription": "Путь к имени пользователя в ID токене", + "idpOidcConfigureScopes": "Области действия", + "idpOidcConfigureScopesDescription": "Список областей OAuth2, разделённых пробелами", + "idpSubmit": "Создать поставщика удостоверений", + "orgPolicies": "Политики организации", "idpSettings": "Настройки {idpName}", - "idpCreateSettingsDescription": "Configure the settings for your identity provider", - "roleMapping": "Role Mapping", - "orgMapping": "Organization Mapping", - "orgPoliciesSearch": "Search organization policies...", - "orgPoliciesAdd": "Add Organization Policy", - "orgRequired": "Organization is required", - "error": "Error", - "success": "Success", - "orgPolicyAddedDescription": "Policy added successfully", - "orgPolicyUpdatedDescription": "Policy updated successfully", - "orgPolicyDeletedDescription": "Policy deleted successfully", - "defaultMappingsUpdatedDescription": "Default mappings updated successfully", - "orgPoliciesAbout": "About Organization Policies", + "idpCreateSettingsDescription": "Настройте параметры для вашего поставщика удостоверений", + "roleMapping": "Сопоставление ролей", + "orgMapping": "Сопоставление организаций", + "orgPoliciesSearch": "Поиск политик организации...", + "orgPoliciesAdd": "Добавить политику организации", + "orgRequired": "Организация обязательна", + "error": "Ошибка", + "success": "Успешно", + "orgPolicyAddedDescription": "Политика успешно добавлена", + "orgPolicyUpdatedDescription": "Политика успешно обновлена", + "orgPolicyDeletedDescription": "Политика успешно удалена", + "defaultMappingsUpdatedDescription": "Сопоставления по умолчанию успешно обновлены", + "orgPoliciesAbout": "О политиках организации", "orgPoliciesAboutDescription": "Политики организации используются для контроля доступа к организациям на основе ID токена пользователя. Вы можете указать выражения JMESPath для извлечения информации о роли и организации из ID токена.", - "orgPoliciesAboutDescriptionLink": "See documentation, for more information.", - "defaultMappingsOptional": "Default Mappings (Optional)", + "orgPoliciesAboutDescriptionLink": "См. документацию для получения дополнительной информации.", + "defaultMappingsOptional": "Сопоставления по умолчанию (необязательно)", "defaultMappingsOptionalDescription": "Сопоставления по умолчанию используются, когда для организации не определена политика организации. Здесь вы можете указать сопоставления ролей и организаций по умолчанию.", - "defaultMappingsRole": "Default Role Mapping", + "defaultMappingsRole": "Сопоставление ролей по умолчанию", "defaultMappingsRoleDescription": "Результат этого выражения должен возвращать имя роли, как определено в организации, в виде строки.", - "defaultMappingsOrg": "Default Organization Mapping", + "defaultMappingsOrg": "Сопоставление организаций по умолчанию", "defaultMappingsOrgDescription": "Это выражение должно возвращать ID организации или true для разрешения доступа пользователя к организации.", - "defaultMappingsSubmit": "Save Default Mappings", - "orgPoliciesEdit": "Edit Organization Policy", - "org": "Organization", - "orgSelect": "Select organization", - "orgSearch": "Search org", - "orgNotFound": "No org found.", - "roleMappingPathOptional": "Role Mapping Path (Optional)", - "orgMappingPathOptional": "Organization Mapping Path (Optional)", - "orgPolicyUpdate": "Update Policy", - "orgPolicyAdd": "Add Policy", - "orgPolicyConfig": "Configure access for an organization", - "idpUpdatedDescription": "Identity provider updated successfully", - "redirectUrl": "Redirect URL", - "redirectUrlAbout": "About Redirect URL", + "defaultMappingsSubmit": "Сохранить сопоставления по умолчанию", + "orgPoliciesEdit": "Редактировать политику организации", + "org": "Организация", + "orgSelect": "Выберите организацию", + "orgSearch": "Поиск организации", + "orgNotFound": "Организация не найдена.", + "roleMappingPathOptional": "Путь сопоставления ролей (необязательно)", + "orgMappingPathOptional": "Путь сопоставления организаций (необязательно)", + "orgPolicyUpdate": "Обновить политику", + "orgPolicyAdd": "Добавить политику", + "orgPolicyConfig": "Настроить доступ для организации", + "idpUpdatedDescription": "Поставщик удостоверений успешно обновлён", + "redirectUrl": "URL редиректа", + "redirectUrlAbout": "О редиректе URL", "redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках вашего поставщика удостоверений.", "pangolinAuth": "Аутентификация - Pangolin", "verificationCodeLengthRequirements": "Ваш код подтверждения должен состоять из 8 символов.", @@ -859,73 +858,73 @@ "accessTokenError": "Ошибка проверки токена доступа", "accessGranted": "Доступ предоставлен", "accessUrlInvalid": "Неверный URL доступа", - "accessGrantedDescription": "You have been granted access to this resource. Redirecting you...", - "accessUrlInvalidDescription": "This shared access URL is invalid. Please contact the resource owner for a new URL.", - "tokenInvalid": "Invalid token", - "pincodeInvalid": "Invalid code", - "passwordErrorRequestReset": "Failed to request reset:", - "passwordErrorReset": "Failed to reset password:", - "passwordResetSuccess": "Password reset successfully! Back to log in...", - "passwordReset": "Reset Password", - "passwordResetDescription": "Follow the steps to reset your password", - "passwordResetSent": "We'll send a password reset code to this email address.", - "passwordResetCode": "Reset Code", - "passwordResetCodeDescription": "Check your email for the reset code.", - "passwordNew": "New Password", - "passwordNewConfirm": "Confirm New Password", - "pincodeAuth": "Authenticator Code", - "pincodeSubmit2": "Submit Code", - "passwordResetSubmit": "Request Reset", - "passwordBack": "Back to Password", - "loginBack": "Go back to log in", - "signup": "Sign up", - "loginStart": "Log in to get started", - "idpOidcTokenValidating": "Validating OIDC token", - "idpOidcTokenResponse": "Validate OIDC token response", - "idpErrorOidcTokenValidating": "Error validating OIDC token", + "accessGrantedDescription": "Вам был предоставлен доступ к этому ресурсу. Перенаправляем вас...", + "accessUrlInvalidDescription": "Этот общий URL доступа недействителен. Пожалуйста, свяжитесь с владельцем ресурса для получения нового URL.", + "tokenInvalid": "Неверный токен", + "pincodeInvalid": "Неверный код", + "passwordErrorRequestReset": "Не удалось запросить сброс:", + "passwordErrorReset": "Не удалось сбросить пароль:", + "passwordResetSuccess": "Пароль успешно сброшен! Вернуться к входу...", + "passwordReset": "Сброс пароля", + "passwordResetDescription": "Следуйте инструкциям для сброса вашего пароля", + "passwordResetSent": "Мы отправим код сброса пароля на этот email адрес.", + "passwordResetCode": "Код сброса пароля", + "passwordResetCodeDescription": "Проверьте вашу почту для получения кода сброса пароля.", + "passwordNew": "Новый пароль", + "passwordNewConfirm": "Подтвердите новый пароль", + "pincodeAuth": "Код аутентификатора", + "pincodeSubmit2": "Отправить код", + "passwordResetSubmit": "Запросить сброс", + "passwordBack": "Назад к паролю", + "loginBack": "Вернуться к входу", + "signup": "Регистрация", + "loginStart": "Войдите для начала работы", + "idpOidcTokenValidating": "Проверка OIDC токена", + "idpOidcTokenResponse": "Проверить ответ OIDC токена", + "idpErrorOidcTokenValidating": "Ошибка проверки OIDC токена", "idpConnectingTo": "Подключение к {name}", - "idpConnectingToDescription": "Validating your identity", - "idpConnectingToProcess": "Connecting...", - "idpConnectingToFinished": "Connected", + "idpConnectingToDescription": "Проверка вашей личности", + "idpConnectingToProcess": "Подключение...", + "idpConnectingToFinished": "Подключено", "idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.", - "idpErrorNotFound": "IdP not found", - "inviteInvalid": "Invalid Invite", - "inviteInvalidDescription": "The invite link is invalid.", - "inviteErrorWrongUser": "Invite is not for this user", - "inviteErrorUserNotExists": "User does not exist. Please create an account first.", - "inviteErrorLoginRequired": "You must be logged in to accept an invite", - "inviteErrorExpired": "The invite may have expired", - "inviteErrorRevoked": "The invite might have been revoked", - "inviteErrorTypo": "There could be a typo in the invite link", - "pangolinSetup": "Setup - Pangolin", - "orgNameRequired": "Organization name is required", - "orgIdRequired": "Organization ID is required", - "orgErrorCreate": "An error occurred while creating org", - "pageNotFound": "Page Not Found", - "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", - "overview": "Overview", - "home": "Home", - "accessControl": "Access Control", - "settings": "Settings", - "usersAll": "All Users", - "license": "License", - "pangolinDashboard": "Dashboard - Pangolin", - "noResults": "No results found.", + "idpErrorNotFound": "IdP не найден", + "inviteInvalid": "Недействительное приглашение", + "inviteInvalidDescription": "Ссылка на приглашение недействительна.", + "inviteErrorWrongUser": "Приглашение не для этого пользователя", + "inviteErrorUserNotExists": "Пользователь не существует. Пожалуйста, сначала создайте учетную запись.", + "inviteErrorLoginRequired": "Вы должны войти, чтобы принять приглашение", + "inviteErrorExpired": "Срок действия приглашения истек", + "inviteErrorRevoked": "Возможно, приглашение было отозвано", + "inviteErrorTypo": "В пригласительной ссылке может быть опечатка", + "pangolinSetup": "Настройка - Pangolin", + "orgNameRequired": "Название организации обязательно", + "orgIdRequired": "ID организации обязателен", + "orgErrorCreate": "Произошла ошибка при создании организации", + "pageNotFound": "Страница не найдена", + "pageNotFoundDescription": "Упс! Страница, которую вы ищете, не существует.", + "overview": "Обзор", + "home": "Главная", + "accessControl": "Контроль доступа", + "settings": "Настройки", + "usersAll": "Все пользователи", + "license": "Лицензия", + "pangolinDashboard": "Дашборд - Pangolin", + "noResults": "Результаты не найдены.", "terabytes": "{count} ТБ", "gigabytes": "{count} ГБ", "megabytes": "{count} МБ", - "tagsEntered": "Entered Tags", - "tagsEnteredDescription": "These are the tags you`ve entered.", - "tagsWarnCannotBeLessThanZero": "maxTags and minTags cannot be less than 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag not allowed as per autocomplete options", - "tagsWarnInvalid": "Invalid tag as per validateTag", - "tagWarnTooShort": "Tag {tagText} is too short", - "tagWarnTooLong": "Tag {tagText} is too long", - "tagsWarnReachedMaxNumber": "Reached the maximum number of tags allowed", - "tagWarnDuplicate": "Duplicate tag {tagText} not added", - "supportKeyInvalid": "Invalid Key", - "supportKeyInvalidDescription": "Your supporter key is invalid.", - "supportKeyValid": "Valid Key", + "tagsEntered": "Введённые теги", + "tagsEnteredDescription": "Это теги, которые вы ввели.", + "tagsWarnCannotBeLessThanZero": "maxTags и minTags не могут быть меньше 0", + "tagsWarnNotAllowedAutocompleteOptions": "Тег не разрешён согласно опциям автозаполнения", + "tagsWarnInvalid": "Недействительный тег согласно validateTag", + "tagWarnTooShort": "Тег {tagText} слишком короткий", + "tagWarnTooLong": "Тег {tagText} слишком длинный", + "tagsWarnReachedMaxNumber": "Достигнуто максимальное количество разрешённых тегов", + "tagWarnDuplicate": "Дублирующий тег {tagText} не добавлен", + "supportKeyInvalid": "Недействительный ключ", + "supportKeyInvalidDescription": "Ваш ключ поддержки недействителен.", + "supportKeyValid": "Действительный ключ", "supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!", "supportKeyErrorValidationDescription": "Failed to validate supporter key.", "supportKey": "Support Development and Adopt a Pangolin!", @@ -994,27 +993,27 @@ "actionListRole": "List Roles", "actionUpdateRole": "Update Role", "actionListAllowedRoleResources": "List Allowed Role Resources", - "actionInviteUser": "Invite User", - "actionRemoveUser": "Remove User", - "actionListUsers": "List Users", - "actionAddUserRole": "Add User Role", - "actionGenerateAccessToken": "Generate Access Token", - "actionDeleteAccessToken": "Delete Access Token", - "actionListAccessTokens": "List Access Tokens", - "actionCreateResourceRule": "Create Resource Rule", - "actionDeleteResourceRule": "Delete Resource Rule", - "actionListResourceRules": "List Resource Rules", - "actionUpdateResourceRule": "Update Resource Rule", - "actionListOrgs": "List Organizations", - "actionCheckOrgId": "Check ID", - "actionCreateOrg": "Create Organization", - "actionDeleteOrg": "Delete Organization", - "actionListApiKeys": "List API Keys", - "actionListApiKeyActions": "List API Key Actions", - "actionSetApiKeyActions": "Set API Key Allowed Actions", - "actionCreateApiKey": "Create API Key", - "actionDeleteApiKey": "Delete API Key", - "actionCreateIdp": "Create IDP", + "actionInviteUser": "Пригласить пользователя", + "actionRemoveUser": "Удалить пользователя", + "actionListUsers": "Список пользователей", + "actionAddUserRole": "Добавить роль пользователя", + "actionGenerateAccessToken": "Сгенерировать токен доступа", + "actionDeleteAccessToken": "Удалить токен доступа", + "actionListAccessTokens": "Список токенов доступа", + "actionCreateResourceRule": "Создать правило ресурса", + "actionDeleteResourceRule": "Удалить правило ресурса", + "actionListResourceRules": "Список правил ресурса", + "actionUpdateResourceRule": "Обновить правило ресурса", + "actionListOrgs": "Список организаций", + "actionCheckOrgId": "Проверить ID", + "actionCreateOrg": "Создать организацию", + "actionDeleteOrg": "Удалить организацию", + "actionListApiKeys": "Список API ключей", + "actionListApiKeyActions": "Список действий API ключа", + "actionSetApiKeyActions": "Установить разрешённые действия API ключа", + "actionCreateApiKey": "Создать API ключ", + "actionDeleteApiKey": "Удалить API ключ", + "actionCreateIdp": "Создать IDP", "actionUpdateIdp": "Обновить IDP", "actionDeleteIdp": "Удалить IDP", "actionListIdps": "Список IDP", @@ -1053,19 +1052,19 @@ "otpErrorDisableDescription": "Произошла ошибка при отключении 2FA", "otpRemove": "Отключить двухфакторную аутентификацию", "otpRemoveDescription": "Отключить двухфакторную аутентификацию для вашей учётной записи", - "otpRemoveSuccess": "Two-Factor Authentication Disabled", + "otpRemoveSuccess": "Двухфакторная аутентификация отключена", "otpRemoveSuccessMessage": "Двухфакторная аутентификация была отключена для вашей учётной записи. Вы можете включить её снова в любое время.", - "otpRemoveSubmit": "Disable 2FA", - "paginator": "Page {current} of {last}", - "paginatorToFirst": "Go to first page", - "paginatorToPrevious": "Go to previous page", - "paginatorToNext": "Go to next page", - "paginatorToLast": "Go to last page", - "copyText": "Copy text", - "copyTextFailed": "Failed to copy text: ", - "copyTextClipboard": "Copy to clipboard", - "inviteErrorInvalidConfirmation": "Invalid confirmation", - "passwordRequired": "Password is required", + "otpRemoveSubmit": "Отключить 2FA", + "paginator": "Страница {current} из {last}", + "paginatorToFirst": "Перейти на первую страницу", + "paginatorToPrevious": "Перейти на предыдущую страницу", + "paginatorToNext": "Перейти на следующую страницу", + "paginatorToLast": "Перейти на последнюю страницу", + "copyText": "Скопировать текст", + "copyTextFailed": "Не удалось скопировать текст: ", + "copyTextClipboard": "Копировать в буфер обмена", + "inviteErrorInvalidConfirmation": "Неверное подтверждение", + "passwordRequired": "Пароль обязателен", "allowAll": "Разрешить всё", "permissionsAllowAll": "Разрешить все разрешения", "githubUsernameRequired": "Имя пользователя GitHub обязательно", @@ -1094,7 +1093,7 @@ "sidebarAllUsers": "Все пользователи", "sidebarIdentityProviders": "Поставщики удостоверений", "sidebarLicense": "Лицензия", - "sidebarClients": "Clients", + "sidebarClients": "Clients (Beta)", "sidebarDomains": "Domains", "enableDockerSocket": "Включить Docker Socket", "enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.", @@ -1162,7 +1161,7 @@ "selectDomainTypeCnameName": "Single Domain (CNAME)", "selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.", "selectDomainTypeWildcardName": "Wildcard Domain", - "selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.", + "selectDomainTypeWildcardDescription": "This domain and its subdomains.", "domainDelegation": "Single Domain", "selectType": "Select a type", "actions": "Actions", @@ -1196,7 +1195,7 @@ "sidebarExpand": "Expand", "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", - "domainPickerEnterDomain": "Enter your domain", + "domainPickerEnterDomain": "Domain", "domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp", "domainPickerDescription": "Enter the full domain of the resource to see available options.", "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", @@ -1206,7 +1205,7 @@ "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", "domainPickerCheckingAvailability": "Checking availability...", - "domainPickerNoMatchingDomains": "No matching domains found for \"{userInput}\". Try a different domain or check your organization's domain settings.", + "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", "domainPickerOrganizationDomains": "Organization Domains", "domainPickerProvidedDomains": "Provided Domains", "domainPickerSubdomain": "Subdomain: {subdomain}", @@ -1266,6 +1265,7 @@ "createDomainName": "Name:", "createDomainValue": "Value:", "createDomainCnameRecords": "CNAME Records", + "createDomainARecords": "A Records", "createDomainRecordNumber": "Record {number}", "createDomainTxtRecords": "TXT Records", "createDomainSaveTheseRecords": "Save These Records", @@ -1273,5 +1273,50 @@ "createDomainDnsPropagation": "DNS Propagation", "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", "resourcePortRequired": "Port number is required for non-HTTP resources", - "resourcePortNotAllowed": "Port number should not be set for HTTP resources" + "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + }, + "siteRequired": "Site is required.", + "olmTunnel": "Olm Tunnel", + "olmTunnelDescription": "Use Olm for client connectivity", + "errorCreatingClient": "Error creating client", + "clientDefaultsNotFound": "Client defaults not found", + "createClient": "Create Client", + "createClientDescription": "Create a new client for connecting to your sites", + "seeAllClients": "See All Clients", + "clientInformation": "Client Information", + "clientNamePlaceholder": "Client name", + "address": "Address", + "subnetPlaceholder": "Subnet", + "addressDescription": "The address that this client will use for connectivity", + "selectSites": "Select sites", + "sitesDescription": "The client will have connectivity to the selected sites", + "clientInstallOlm": "Install Olm", + "clientInstallOlmDescription": "Get Olm running on your system", + "clientOlmCredentials": "Olm Credentials", + "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", + "olmEndpoint": "Olm Endpoint", + "olmId": "Olm ID", + "olmSecretKey": "Olm Secret Key", + "clientCredentialsSave": "Save Your Credentials", + "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", + "generalSettingsDescription": "Configure the general settings for this client", + "clientUpdated": "Client updated", + "clientUpdatedDescription": "The client has been updated.", + "clientUpdateFailed": "Failed to update client", + "clientUpdateError": "An error occurred while updating the client.", + "sitesFetchFailed": "Failed to fetch sites", + "sitesFetchError": "An error occurred while fetching sites.", + "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", + "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", + "remoteSubnets": "Remote Subnets", + "enterCidrRange": "Enter CIDR range", + "remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.", + "resourceEnableProxy": "Enable Public Proxy", + "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", + "externalProxyEnabled": "External Proxy Enabled" } From 92e69f561f71f9214aba795226f16b78a9a10255 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 31 Jul 2025 11:04:15 -0700 Subject: [PATCH 64/64] Org is not optional --- server/routers/client/getClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 46f31b8c..b1927788 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -14,7 +14,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const getClientSchema = z .object({ clientId: z.string().transform(stoi).pipe(z.number().int().positive()), - orgId: z.string().optional() + orgId: z.string() }) .strict();